Skip to content

Commit 39fef4b

Browse files
authored
feat: batch renames, Python import rewrites and repairs of existing rule failures in sqb rename and sqb mv (#1229)
* feat: batch model renames, Python import rewrites and repairs of existing rule failures in sqb rename and sqb mv sqb rename takes several model TARGET NEW_NAME pairs, or --from-file, and plans and verifies them together like batch moves: one compile before, one of the edited project after, all or nothing. Each renamed model gets its own migrate_from, so every old name keeps a compatibility view. Column renames and --cascade stay single and are refused in batch form; name reuse stays refused. The scratch copy and the real write now apply changes in the same order: write every change, then remove only the moved sources no change writes to, so a chained move cannot lose a file in the real write while verification passes. sqb mv rewrites project Python imports of the Python files it moves (direct, aliased, grouped, from-package and relative imports) through a reusable import-rewrite function, and refuses with the file and line when an import cannot be rewritten safely: an unaliased import, a star import from the moved module's package, a dynamic import naming it, or a mixed from-package import. A project that already fails project rules can be refactored: existing rule failures no longer block planning, and the edited project is compared with them. New or worse rule failures, and every other error, still block. Text and JSON output, including --dry-run, explain the failures before and after. Projects failing at the input stage, such as a duplicate macro name, are refused cleanly with their compile error, and --json prints an error document on stdout when the project does not compile or the request is invalid. * fix: plan SQL tests beside existing rule failures and block any growth in a rule failure With rule failures in the project, the compiles of sqb rename and sqb mv skipped SQL-test planning, so a planning error could pass verification. They now plan SQL tests whenever the only errors are rule failures, as sqb compile does; planning errors block. A rule failure of the edited project is existing only with the same code, place and message, or when only its numbers changed and none grew. Any other change blocks, wherever it is reported, so a failure reported at a sample file can no longer grow unnoticed. Nothing counts as resolved when another error may have kept rules from running. Python import rewriting also refuses importing a moved module by name from a package whose __init__.py binds that name, or when the move renames the module, and refuses a move while a project Python file is not UTF-8. sqb rename with a target and no new name, or with nothing, is again a usage error with exit code 2. * test: regenerate refactor goldens for the rule baseline report
1 parent 368a23e commit 39fef4b

82 files changed

Lines changed: 3413 additions & 311 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/sqlbuild-cli/src/argv/_helpers/normalization.rs‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use crate::argv::constants::{
55
COMMAND_DEST, CONCURRENCY_COMMANDS, CONCURRENCY_ENV_VAR, CURSOR_COMMANDS, DBT_COMMAND,
66
DBT_DEBUG_COMMAND, DBT_PASSTHROUGH_COMMANDS, DBT_PASSTHROUGH_SEPARATOR, DBT_VERBOSE_OPTION,
77
DBT_VERBOSE_OPTIONS, DEBUG_OPTION, INT_CONVERTER, JANITOR_COMMAND, NO_COLOR_OPTION,
8-
POSITIONAL_SELECT_COMMANDS, SCOPE_COMMAND, SCOPE_DEFAULT_PAGE_SIZE, SCOPE_GLOBAL_SUMMARY,
9-
SCOPE_KIND_DEST, TEST_COMMAND,
8+
POSITIONAL_SELECT_COMMANDS, RENAME_COMMAND, SCOPE_COMMAND, SCOPE_DEFAULT_PAGE_SIZE,
9+
SCOPE_GLOBAL_SUMMARY, SCOPE_KIND_DEST, TEST_COMMAND,
1010
};
1111
use crate::argv::models::{Namespace, ParserSpec, Value};
1212

@@ -96,6 +96,12 @@ impl Run<'_> {
9696
));
9797
}
9898
}
99+
if command == RENAME_COMMAND
100+
&& let Some(message) = rename_error(&namespace)
101+
{
102+
let rename: &ParserSpec = subcommand_parser(parser, RENAME_COMMAND).unwrap_or(parser);
103+
return Err(self.error(rename, &message));
104+
}
99105
if command == SCOPE_COMMAND
100106
&& let Some(message) = scope_error(parser, &namespace)
101107
{
@@ -263,3 +269,28 @@ fn integer(value: &Value) -> i128 {
263269
i128::MAX
264270
})
265271
}
272+
273+
/// The usage error of a `sqb rename` without `--from-file` and without one full pair.
274+
fn rename_error(namespace: &Namespace) -> Option<String> {
275+
if namespace.value("refactor_from_file") != Value::None
276+
|| namespace.value("refactor_new_name") != Value::None
277+
{
278+
return None;
279+
}
280+
let missing: &str = if namespace.value("refactor_target") == Value::None {
281+
"TARGET, NEW_NAME"
282+
} else {
283+
"NEW_NAME"
284+
};
285+
Some(format!("the following arguments are required: {missing}"))
286+
}
287+
288+
/// The parser of sub-command `name` below `parser`.
289+
fn subcommand_parser<'a>(parser: &'a ParserSpec, name: &str) -> Option<&'a ParserSpec> {
290+
parser
291+
.actions
292+
.iter()
293+
.flat_map(|action| &action.parsers)
294+
.find(|(command, _)| command == name)
295+
.map(|(_, spec)| spec)
296+
}

‎crates/sqlbuild-cli/src/argv/command_table.json‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4891,18 +4891,37 @@
48914891
{
48924892
"kind": "store",
48934893
"dest": "refactor_target",
4894-
"required": true,
4894+
"nargs": "?",
48954895
"help": "<model> or <model>.<column>; the model: and column: prefixes are optional",
48964896
"metavar": "TARGET",
48974897
"group": 0
48984898
},
48994899
{
49004900
"kind": "store",
49014901
"dest": "refactor_new_name",
4902-
"required": true,
4902+
"nargs": "?",
49034903
"metavar": "NEW_NAME",
49044904
"group": 0
49054905
},
4906+
{
4907+
"kind": "store",
4908+
"dest": "refactor_more_moves",
4909+
"nargs": "*",
4910+
"default": [],
4911+
"help": "further model TARGET NEW_NAME pairs, renamed together with the first",
4912+
"metavar": "MORE",
4913+
"group": 0
4914+
},
4915+
{
4916+
"kind": "store",
4917+
"options": [
4918+
"--from-file"
4919+
],
4920+
"dest": "refactor_from_file",
4921+
"help": "read model TARGET NEW_NAME pairs from FILE, one per line; # starts a comment",
4922+
"metavar": "FILE",
4923+
"group": 1
4924+
},
49064925
{
49074926
"kind": "store_true",
49084927
"options": [

‎crates/sqlbuild-cli/src/argv/constants.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub(crate) const POSITIONAL_SELECT_COMMANDS: [&str; 8] = [
5151
];
5252
pub(crate) const CONCURRENCY_COMMANDS: [&str; 5] = ["audit", "build", "load", "seed", "test"];
5353
pub(crate) const CURSOR_COMMANDS: [&str; 3] = ["build", "load", "plan"];
54+
pub(crate) const RENAME_COMMAND: &str = "rename";
5455
pub(crate) const SCOPE_COMMAND: &str = "scope";
5556
pub(crate) const TEST_COMMAND: &str = "test";
5657
pub(crate) const JANITOR_COMMAND: &str = "janitor";

‎crates/sqlbuild-cli/src/argv/tests/test_parse_cli_arguments.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ fn given_help_version_or_rejected_arguments_when_parsing_then_exits_after_writin
8888
expected_exit: 2,
8989
expected_last_write: "sqb rules: error[C900]: the following arguments are required: rules_command\n\n",
9090
},
91+
ExitTestCase {
92+
description: "a rename target without a new name stays a usage error",
93+
argv: &["rename", "stg_orders"],
94+
expected_exit: 2,
95+
expected_last_write: "sqb rename: error[C900]: the following arguments are required: NEW_NAME\n\n",
96+
},
97+
ExitTestCase {
98+
description: "a rename without a target or a file stays a usage error",
99+
argv: &["rename"],
100+
expected_exit: 2,
101+
expected_last_write: "sqb rename: error[C900]: the following arguments are required: TARGET, NEW_NAME\n\n",
102+
},
91103
];
92104

93105
for test_case in test_cases {

‎crates/sqlbuild-cli/src/dispatch/main/command_dispatch.rs‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use crate::dispatch::constants::{
99
LINEAGE_ROUTE, LOAD_ROUTE, PLAN_ROUTE, PLAYGROUND_ROUTE, QUERY_ROUTE, RECALL_COMMAND_NAME,
1010
RECALL_ROUTE, REFACTOR_ROUTE, RULES_ROUTE, SCOPE_ROUTE, SEED_ROUTE, SKILLS_ROUTE, TEST_ROUTE,
1111
};
12-
use crate::dispatch::main::unavailable_handler::unavailable_handler;
1312
use crate::dispatch::models::{CommandFacts, Dispatch, DispatchFailure, Route};
1413

1514
/// The route `facts` select, checked in the order the Python dispatcher checked them.
@@ -21,11 +20,7 @@ pub fn command_dispatch(facts: &CommandFacts) -> Dispatch {
2120
let checked =
2221
|route: &'static Route, failure: Option<DispatchFailure>| Dispatch::Run(route, failure);
2322
match command {
24-
"rename" => checked(
25-
&REFACTOR_ROUTE,
26-
(!facts.refactor_target).then(|| unavailable_handler(command, "C950")),
27-
),
28-
"mv" => run(&REFACTOR_ROUTE),
23+
"rename" | "mv" => run(&REFACTOR_ROUTE),
2924
"contract" => checked(
3025
&CONTRACT_ROUTE,
3126
(!facts.contract_ready).then(|| {

‎crates/sqlbuild-cli/src/dispatch/models.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ pub struct CommandFacts {
9797
pub command: Option<String>,
9898
pub subcommand: Option<String>,
9999
pub json: bool,
100-
pub refactor_target: bool,
101100
pub from_target: bool,
102101
pub contract_ready: bool,
103102
pub format_diff: bool,

‎crates/sqlbuild-cli/src/dispatch/tests/helpers.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ pub(super) fn facts(command: &str, subcommand: Option<&str>) -> CommandFacts {
77
CommandFacts {
88
command: Some(command.to_owned()),
99
subcommand: subcommand.map(str::to_owned),
10-
refactor_target: true,
1110
from_target: true,
1211
contract_ready: true,
1312
..CommandFacts::default()

‎crates/sqlbuild-cli/src/dispatch/tests/test_command_dispatch.rs‎

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,22 +89,13 @@ fn given_parsed_command_when_dispatching_then_selects_python_dispatcher_route()
8989
expected_dispatch: runs(&SCENARIO_CAPTURE_ROUTE),
9090
},
9191
CommandDispatchTestCase {
92-
description: "rename without a target fails",
93-
facts: CommandFacts {
94-
refactor_target: false,
95-
..facts("rename", None)
96-
},
97-
expected_dispatch: Dispatch::Run(
98-
&REFACTOR_ROUTE,
99-
Some(failure("rename command handler is unavailable", "C950")),
100-
),
92+
description: "rename reads its pairs itself, so they may come only from a file",
93+
facts: facts("rename", None),
94+
expected_dispatch: runs(&REFACTOR_ROUTE),
10195
},
10296
CommandDispatchTestCase {
103-
description: "mv reads its pairs itself, so it may come only from a file",
104-
facts: CommandFacts {
105-
refactor_target: false,
106-
..facts("mv", None)
107-
},
97+
description: "mv reads its pairs itself, so they may come only from a file",
98+
facts: facts("mv", None),
10899
expected_dispatch: runs(&REFACTOR_ROUTE),
109100
},
110101
CommandDispatchTestCase {

0 commit comments

Comments
 (0)