Skip to content

Commit e2a2b2e

Browse files
committed
fix: make model: and column: prefixes optional in sqb rename and sqb mv
Resolve rename and mv targets with the same resolver sqb lineage uses, so a bare <model> renames or moves the model and <model>.<column> renames the column. Prefixed forms keep working. Targets naming a source, seed or function, or a prefix that contradicts the resource, fail with C954 and say what was found. sqb lineage now also accepts the column: prefix.
1 parent f3ecd83 commit e2a2b2e

24 files changed

Lines changed: 589 additions & 88 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ The playground runs on local DuckDB, with no warehouse credentials.
6666
update every reference, and the next build
6767
[migrates](https://sqlbuild.com/docs/concepts/models/migrations/) the existing table instead of
6868
rebuilding it. The old name keeps working through a compatibility view.
69-
`sqb rename column:<model>.<column>` renames an incremental model's column in place.
69+
`sqb rename <model>.<column>` renames an incremental model's column in place.
7070
- **Replay on change.** When a model's SQL changes, choose how far back to reprocess, from only the
7171
new data to the last 14 days to a full rebuild, with
7272
[`replay_on_change`](https://sqlbuild.com/docs/concepts/incremental/#replay-on-change).

‎src/sqlbuild/.agents/skills/sqlbuild/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ the trigger applies.
8282
| Know what a model can see: which macros, enums and constants are visible, used, or unavailable | `sqb scope model:<name>` |
8383
| Fix "unknown macro/enum/constant", or decide where a declaration should live | `sqb scope model:<name> --explain macro:<name>` |
8484
| Preview whether moving a file breaks declaration visibility | `sqb scope model:<name> --as-path <new/path.sql>` |
85-
| Rename a model, move a model file, or rename a column | `sqb rename model:<old> <new>`, `sqb mv model:<name> <path/or/folder/>`, `sqb rename column:<model>.<column> <new>` |
85+
| Rename a model, move a model file, or rename a column | `sqb rename <old> <new>`, `sqb mv <name> <path/or/folder/>`, `sqb rename <model>.<column> <new>` (`model:`/`column:` prefixes optional) |
8686
| Find what a model depends on, or what breaks if it changes | `sqb lineage <model> --direction both` |
8787
| Trace where a column comes from, or who consumes it before renaming or dropping it | `sqb lineage <model>.<column> --direction downstream` |
8888
| See which intermediate models a SQL test will really execute | `sqb test --select <model> --inspect` |

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/rename.md‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,21 @@ Nothing is written until the edited project compiles. The command applies its ed
2424
## Usage
2525

2626
```bash
27-
sqb rename model:<name> <new_name> [flags]
28-
sqb mv model:<name> <path/to/new_file.sql | folder/> [flags]
29-
sqb rename column:<model>.<column> <new_name> [--cascade] [flags]
27+
sqb rename <model> <new_name> [flags]
28+
sqb mv <model> <path/to/new_file.sql | folder/> [flags]
29+
sqb rename <model>.<column> <new_name> [--cascade] [flags]
3030
```
3131

3232
```bash
33-
sqb rename model:stg_orders stg_order_lines
34-
sqb mv model:fact_orders models/reporting/ # keeps the name
35-
sqb mv model:fact_orders models/reporting/order_facts.sql # moves and renames
36-
sqb rename column:stg_orders.amount revenue
37-
sqb rename column:stg_orders.amount revenue --cascade
33+
sqb rename stg_orders stg_order_lines
34+
sqb mv fact_orders models/reporting/ # keeps the name
35+
sqb mv fact_orders models/reporting/order_facts.sql # moves and renames
36+
sqb rename stg_orders.amount revenue
37+
sqb rename stg_orders.amount revenue --cascade
3838
```
3939

40+
Targets are written the way [`sqb lineage`](lineage.md) reads them: a bare name is a model, and `<model>.<column>` is one of its columns. The `model:` and `column:` prefixes are optional, so `sqb rename model:stg_orders stg_order_lines` and `sqb rename column:stg_orders.amount revenue` still work. A target that names a source, seed, or function is refused, because those are not renamed by these commands.
41+
4042
A model rename keeps the file in its folder and renames it to `<new_name>.sql`. `sqb mv` takes a new file path, which also sets the model name, or a folder ending in `/`, which keeps it.
4143

4244
## What changes

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/models/column-migrations.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Column migrations apply to incremental models, including microbatch models, and
2121

2222
## Declaring a rename
2323

24-
[`sqb rename column:<model>.<column>`](../../cli/rename.md) renames the column in the model and every query that reads it, and adds the `migrate_from` declaration below for you.
24+
[`sqb rename <model>.<column>`](../../cli/rename.md) renames the column in the model and every query that reads it, and adds the `migrate_from` declaration below for you.
2525

2626
Add `migrate_from` to the renamed column in the model header, naming the old column:
2727

‎src/sqlbuild/cli/commands/_helpers/entry/parser.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ def _add_refactor_parsers(
153153
CliCommand.RENAME, help="rename a model or column and update every reference"
154154
)
155155
rename_parser.add_argument(
156-
"refactor_target", metavar="TARGET", help="model:<name> or column:<model>.<column>"
156+
"refactor_target",
157+
metavar="TARGET",
158+
help="<model> or <model>.<column>; the model: and column: prefixes are optional",
157159
)
158160
rename_parser.add_argument("refactor_new_name", metavar="NEW_NAME")
159161
rename_parser.add_argument(
@@ -166,7 +168,9 @@ def _add_refactor_parsers(
166168
move_parser: argparse.ArgumentParser = subparsers.add_parser(
167169
CliCommand.MV, help="move a model file, optionally renaming it, and update references"
168170
)
169-
move_parser.add_argument("refactor_target", metavar="TARGET", help="model:<name>")
171+
move_parser.add_argument(
172+
"refactor_target", metavar="TARGET", help="<model>; the model: prefix is optional"
173+
)
170174
move_parser.add_argument(
171175
"refactor_destination", metavar="DESTINATION", help="new .sql path or folder/"
172176
)

‎src/sqlbuild/cli/commands/_helpers/lineage/selection.py‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from sqlbuild.cli.commands.constants import (
99
BOTH_DIRECTIONS,
10+
COLUMN_TARGET_KIND,
1011
COLUMN_TARGET_SEPARATOR,
1112
DOWNSTREAM_DIRECTION,
1213
PATH_BETWEEN_MARKER,
@@ -25,6 +26,7 @@
2526
LineageGraph,
2627
LineageNode,
2728
LineageSelectionAnchors,
29+
LineageTarget,
2830
ParsedLineagePathSelector,
2931
ParsedLineageSelector,
3032
RelationLineageIndex,
@@ -64,6 +66,7 @@ class _ColumnLineageCandidateSelection:
6466
str(CompiledResourceType.SEED),
6567
str(CompiledResourceType.UDF),
6668
str(CompiledResourceType.TABLE_FN),
69+
COLUMN_TARGET_KIND,
6770
}
6871
)
6972

@@ -85,14 +88,39 @@ def parse_depth(raw_depth: str) -> int | None:
8588
def normalize_lineage_target(*, graph: ProjectGraph | RelationLineageIndex, target: str) -> str:
8689
"""Strip a printed kind prefix such as `model:` after checking it matches the resource."""
8790

88-
kind, separator, remainder = target.partition(_KIND_SEPARATOR)
89-
if not separator or kind not in _TARGET_KINDS:
91+
resolved: LineageTarget = resolve_lineage_target(all_keys=graph.all_keys, target=target)
92+
if resolved.kind is None:
9093
return target
91-
name: str = remainder.split(COLUMN_TARGET_SEPARATOR, maxsplit=1)[0]
92-
key: CompiledObjectKey | None = graph.all_keys.get(name)
93-
if key is None or str(key.resource_type) != kind:
94+
if not resolved.kind_matches:
9495
raise _unknown_target_error(target)
95-
return remainder
96+
return resolved.name
97+
98+
99+
def resolve_lineage_target(*, all_keys: dict[str, CompiledObjectKey], target: str) -> LineageTarget:
100+
"""Resolve `[kind:]<name>` or `[kind:]<model>.<column>` against project resource names."""
101+
102+
kind, separator, remainder = target.partition(_KIND_SEPARATOR)
103+
written_kind: str | None = kind if separator and kind in _TARGET_KINDS else None
104+
name: str = remainder if written_kind is not None else target
105+
resource_name, column_separator, column_name = name.partition(COLUMN_TARGET_SEPARATOR)
106+
key: CompiledObjectKey | None = all_keys.get(resource_name)
107+
column: str | None = column_name if column_separator else None
108+
kind_matches: bool
109+
if written_kind is None:
110+
kind_matches = True
111+
elif written_kind == COLUMN_TARGET_KIND:
112+
kind_matches = (
113+
key is not None and key.resource_type == CompiledResourceType.MODEL and bool(column)
114+
)
115+
else:
116+
kind_matches = key is not None and str(key.resource_type) == written_kind
117+
return LineageTarget(
118+
kind=written_kind,
119+
name=name,
120+
key=key,
121+
column_name=column,
122+
kind_matches=kind_matches,
123+
)
96124

97125

98126
def select_target_lineage(

‎src/sqlbuild/cli/commands/_helpers/refactor/command.py‎

Lines changed: 4 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,10 @@
1414
render_refactor_json,
1515
render_refactor_text,
1616
)
17+
from sqlbuild.cli.commands._helpers.refactor.target import refactor_request_for_target
1718
from sqlbuild.cli.commands.exceptions import CliUserError
1819
from sqlbuild.cli.commands.models import RefactorCommandRequest, RefactorCompile
19-
from sqlbuild.cli.commands.types import CliCommand
2020
from sqlbuild.compiler.compile.models import CompilerDiagnostic
21-
from sqlbuild.compiler.refactoring.constants import (
22-
COLUMN_KIND_PREFIX,
23-
COLUMN_TARGET_SEPARATOR,
24-
MODEL_KIND_PREFIX,
25-
)
2621
from sqlbuild.compiler.refactoring.exceptions import RefactorInputError
2722
from sqlbuild.compiler.refactoring.main.commit_refactor_plan import commit_refactor_plan
2823
from sqlbuild.compiler.refactoring.main.plan_column_rename import plan_column_rename
@@ -69,7 +64,9 @@ def _run(
6964
_write_errors(errors=before.errors)
7065
return 1
7166
status.complete(message=f"Compiled project. ({time.monotonic() - started:.2f}s)")
72-
refactor_request: RefactorRequest = _refactor_request(request=request)
67+
refactor_request: RefactorRequest = refactor_request_for_target(
68+
request=request, all_keys=before.project.graph.all_keys
69+
)
7370
status.start("Planning edits...")
7471
plan: RefactorPlan = (
7572
plan_column_rename(project=before.project, request=refactor_request)
@@ -171,50 +168,6 @@ def _files(plan: RefactorPlan) -> str:
171168
return format_count_noun(count=len(plan.changes), singular="file")
172169

173170

174-
def _refactor_request(*, request: RefactorCommandRequest) -> RefactorRequest:
175-
target: str = request.target.strip()
176-
if request.command == CliCommand.MV:
177-
if not target.startswith(MODEL_KIND_PREFIX):
178-
raise CliUserError(
179-
f"sqb mv moves models; got '{target}'",
180-
code="C954",
181-
help="write the target as model:<name>",
182-
)
183-
return RefactorRequest(
184-
operation=RefactorOperation.MOVE_MODEL,
185-
model_name=target.removeprefix(MODEL_KIND_PREFIX),
186-
new_name="",
187-
destination=request.destination or "",
188-
)
189-
new_name: str = (request.new_name or "").strip()
190-
if target.startswith(MODEL_KIND_PREFIX):
191-
if request.cascade:
192-
raise CliUserError("--cascade applies to column renames only", code="C954")
193-
return RefactorRequest(
194-
operation=RefactorOperation.RENAME_MODEL,
195-
model_name=target.removeprefix(MODEL_KIND_PREFIX),
196-
new_name=new_name,
197-
)
198-
if target.startswith(COLUMN_KIND_PREFIX) and COLUMN_TARGET_SEPARATOR in target:
199-
model_name: str
200-
column_name: str
201-
model_name, column_name = target.removeprefix(COLUMN_KIND_PREFIX).rsplit(
202-
COLUMN_TARGET_SEPARATOR, 1
203-
)
204-
return RefactorRequest(
205-
operation=RefactorOperation.RENAME_COLUMN,
206-
model_name=model_name,
207-
column_name=column_name,
208-
new_name=new_name,
209-
cascade=request.cascade,
210-
)
211-
raise CliUserError(
212-
f"cannot rename '{target}'",
213-
code="C954",
214-
help="write the target as model:<name> or column:<model>.<column>",
215-
)
216-
217-
218171
def _relative(
219172
*, errors: tuple[CompilerDiagnostic, ...], staging_dir: Path
220173
) -> tuple[CompilerDiagnostic, ...]:
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Read the target of `sqb rename` and `sqb mv` the way `sqb lineage` reads targets."""
2+
3+
from __future__ import annotations
4+
5+
from sqlbuild.cli.commands._helpers.lineage.selection import resolve_lineage_target
6+
from sqlbuild.cli.commands.constants import COLUMN_TARGET_KIND, COLUMN_TARGET_SEPARATOR
7+
from sqlbuild.cli.commands.exceptions import CliUserError
8+
from sqlbuild.cli.commands.models import LineageTarget, RefactorCommandRequest
9+
from sqlbuild.cli.commands.types import CliCommand
10+
from sqlbuild.compiler.compile.models import CompiledObjectKey
11+
from sqlbuild.compiler.compile.types import CompiledResourceType
12+
from sqlbuild.compiler.refactoring.models import RefactorRequest
13+
from sqlbuild.compiler.refactoring.types import RefactorOperation
14+
15+
_RENAME_USAGE: str = (
16+
"write the target as <model> or <model>.<column>; the model: and column: prefixes are optional"
17+
)
18+
_MOVE_USAGE: str = "write the target as <model>; the model: prefix is optional"
19+
_RENAME_SCOPE: str = "sqb rename renames models and model columns"
20+
_MOVE_SCOPE: str = "sqb mv moves models"
21+
22+
23+
def refactor_request_for_target(
24+
*, request: RefactorCommandRequest, all_keys: dict[str, CompiledObjectKey]
25+
) -> RefactorRequest:
26+
"""Read a `sqb rename` or `sqb mv` target the way `sqb lineage` reads its targets."""
27+
28+
raw: str = request.target.strip()
29+
target: LineageTarget = resolve_lineage_target(all_keys=all_keys, target=raw)
30+
is_move: bool = request.command == CliCommand.MV
31+
_check_target(raw=raw, target=target, is_move=is_move)
32+
model_name: str = target.name.partition(COLUMN_TARGET_SEPARATOR)[0]
33+
if is_move:
34+
return RefactorRequest(
35+
operation=RefactorOperation.MOVE_MODEL,
36+
model_name=model_name,
37+
new_name="",
38+
destination=request.destination or "",
39+
)
40+
new_name: str = (request.new_name or "").strip()
41+
if target.column_name is None:
42+
if request.cascade:
43+
raise CliUserError("--cascade applies to column renames only", code="C954")
44+
return RefactorRequest(
45+
operation=RefactorOperation.RENAME_MODEL,
46+
model_name=model_name,
47+
new_name=new_name,
48+
)
49+
return RefactorRequest(
50+
operation=RefactorOperation.RENAME_COLUMN,
51+
model_name=model_name,
52+
column_name=target.column_name,
53+
new_name=new_name,
54+
cascade=request.cascade,
55+
)
56+
57+
58+
def _check_target(*, raw: str, target: LineageTarget, is_move: bool) -> None:
59+
verb: str = "move" if is_move else "rename"
60+
usage: str = _MOVE_USAGE if is_move else _RENAME_USAGE
61+
key: CompiledObjectKey | None = target.key
62+
if not target.name or (target.column_name is not None and not target.column_name):
63+
raise CliUserError(f"cannot {verb} '{raw}'", code="C954", help=usage)
64+
if key is not None and key.resource_type != CompiledResourceType.MODEL:
65+
raise CliUserError(
66+
f"cannot {verb} '{raw}': {key.name} is a {key.resource_type}; "
67+
f"{_MOVE_SCOPE if is_move else _RENAME_SCOPE}",
68+
code="C954",
69+
help=usage,
70+
)
71+
if key is not None and not target.kind_matches:
72+
found: str = (
73+
f"column: needs <model>.<column>, and {key.name} names a model"
74+
if target.kind == COLUMN_TARGET_KIND
75+
else f"{key.name} is a {key.resource_type}, not a {target.kind}"
76+
)
77+
raise CliUserError(f"cannot {verb} '{raw}': {found}", code="C954", help=usage)
78+
if is_move and target.column_name is not None:
79+
raise CliUserError(
80+
f"sqb mv moves models; '{raw}' names a column",
81+
code="C954",
82+
help=f"rename a column with sqb rename {target.name} <new_name>",
83+
)

‎src/sqlbuild/cli/commands/constants.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
PATH_SEPARATOR: str = "/"
4343
SELECTOR_EXPANSION_MARKER: str = "+"
4444
COLUMN_TARGET_SEPARATOR: str = "."
45+
COLUMN_TARGET_KIND: str = "column"
4546
SUPPORTED_TYPED_SELECTOR_KINDS: frozenset[str] = frozenset(
4647
{
4748
SelectorKind.SEED,

‎src/sqlbuild/cli/commands/models.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,17 @@ class LineageSelectionAnchors:
764764
retained: frozenset[CompiledObjectKey] = field(default_factory=frozenset)
765765

766766

767+
@dataclass(frozen=True)
768+
class LineageTarget:
769+
"""One positional target, `[kind:]<name>` or `[kind:]<model>.<column>`, resolved by name."""
770+
771+
kind: str | None
772+
name: str
773+
key: CompiledObjectKey | None
774+
column_name: str | None
775+
kind_matches: bool
776+
777+
767778
@dataclass(frozen=True)
768779
class ParsedLineageSelector:
769780
"""One parsed non-path lineage selector."""

0 commit comments

Comments
 (0)