Repository: dbt-labs/dbt-core

Issue:
[CT-2622] [Pre-Regression] `run-operation` requires the name of the package to run macros from 1.6b2
### Is this a regression in a recent version of dbt-core?

- [X] I believe this is a regression in dbt-core functionality
- [X] I have searched the existing issues, and I could not find an existing issue for this regression

### Current Behavior

Since 1.6b2, `run-operation` requires the name of the package when the macro is from another package, otherwise an error is raised.

As part of the integration tests check for dbt-external-table, we do a `dbt run-operation stage_external_sources`. This command used to work until 1.6b1 but form 1.6b2 it started raising

```
dbt could not find a macro with the name "stage_external_sources" in the "dbt_external_tables_integration_tests" package
```

The current workaround I found was to qualify the name of the macro with the package it comes from: `dbt run-operation dbt_external_tables.stage_external_sources`

You can see all the details in the different Circle CI checks for [this PR](https://github.com/dbt-labs/dbt-external-tables/pull/211)

### Expected/Previous Behavior

It shoud either be listed as a breaking change or we should still be able to call the macro without the package name

### Steps To Reproduce

Described under "Current Behavior"

### Relevant log output

```shell
see Circle CI logs
```


### Environment

```markdown
- OS: NA
- Python: NA
- dbt (working version): 1.6b1
- dbt (regression version): 1.6b2
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

_No response_
[CT-2622] [Pre-Regression] `run-operation` requires the name of the package to run macros from 1.6b2
### Is this a regression in a recent version of dbt-core?

- [X] I believe this is a regression in dbt-core functionality
- [X] I have searched the existing issues, and I could not find an existing issue for this regression

### Current Behavior

Since 1.6b2, `run-operation` requires the name of the package when the macro is from another package, otherwise an error is raised.

As part of the integration tests check for dbt-external-table, we do a `dbt run-operation stage_external_sources`. This command used to work until 1.6b1 but form 1.6b2 it started raising

```
dbt could not find a macro with the name "stage_external_sources" in the "dbt_external_tables_integration_tests" package
```

The current workaround I found was to qualify the name of the macro with the package it comes from: `dbt run-operation dbt_external_tables.stage_external_sources`

You can see all the details in the different Circle CI checks for [this PR](https://github.com/dbt-labs/dbt-external-tables/pull/211)

### Expected/Previous Behavior

It shoud either be listed as a breaking change or we should still be able to call the macro without the package name

### Steps To Reproduce

Described under "Current Behavior"

### Relevant log output

```shell
see Circle CI logs
```


### Environment

```markdown
- OS: NA
- Python: NA
- dbt (working version): 1.6b1
- dbt (regression version): 1.6b2
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

_No response_


Discussion:
Thanks for opening @b-per!

I'm pretty sure this was caused by the changes in #7655 — specifically [this line](https://github.com/dbt-labs/dbt-core/pull/7655/files#diff-0fc1d0fbe4bad67c1dac5ba48b3db8f45e8015adc673253987a973933079ccceR31), which now expects to look in the current root project if a package name is not passed explicitly.

To preserve the existing behavior, we should use our macro resolution logic instead,  the same that's used for the lookup within `adapter.execute_macro`: [`manifest.find_macro_by_name`](https://github.com/dbt-labs/dbt-core/blob/8e1c4ec116e8968eb1cec39e713dbebed0e6a76c/core/dbt/contracts/graph/manifest.py#L587).

Something like:
```python
        if "." in macro_name:
            package_name, macro_name = macro_name.split(".", 1)
        else:
            package_name = None
            
       ...

        macro = self.manifest.find_macro_by_name(  # type: ignore[union-attr]
            macro_name, self.config.project_name, package_name
        )
        unique_id = macro.unique_id
```

### Minimal repro case

```sql
-- pkg/macros/macro.sql
{% macro something_cool() %}
  {{ log("something cool", info=true) }}
{% endmacro %}
```
```yaml
# packages.yml
packages:
  - local: pkg
```
**Before (current v1.6.0b2)**
```
$ dbt run-operation something_cool
13:20:52  Running with dbt=1.6.0-b2
13:20:52  Found 1 model, 0 tests, 0 snapshots, 0 analyses, 314 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups
13:20:52  Encountered an error while running operation: Runtime Error
  dbt could not find a macro with the name "something_cool" in the "my_dbt_project" package
$ dbt run-operation pkg.something_cool
13:20:58  Running with dbt=1.6.0-b2
13:20:59  Found 1 model, 0 tests, 0 snapshots, 0 analyses, 314 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups
13:20:59  something cool
```
**After (fixed with change above)**
```
$ dbt run-operation something_cool
13:16:49  Running with dbt=1.6.0-b2
13:16:49  Found 1 model, 0 tests, 0 snapshots, 0 analyses, 314 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups
13:16:49  something cool
```
Thanks for opening @b-per!

I'm pretty sure this was caused by the changes in #7655 — specifically [this line](https://github.com/dbt-labs/dbt-core/pull/7655/files#diff-0fc1d0fbe4bad67c1dac5ba48b3db8f45e8015adc673253987a973933079ccceR31), which now expects to look in the current root project if a package name is not passed explicitly.

To preserve the existing behavior, we should use our macro resolution logic instead,  the same that's used for the lookup within `adapter.execute_macro`: [`manifest.find_macro_by_name`](https://github.com/dbt-labs/dbt-core/blob/8e1c4ec116e8968eb1cec39e713dbebed0e6a76c/core/dbt/contracts/graph/manifest.py#L587).

Something like:
```python
        if "." in macro_name:
            package_name, macro_name = macro_name.split(".", 1)
        else:
            package_name = None
            
       ...

        macro = self.manifest.find_macro_by_name(  # type: ignore[union-attr]
            macro_name, self.config.project_name, package_name
        )
        unique_id = macro.unique_id
```

### Minimal repro case

```sql
-- pkg/macros/macro.sql
{% macro something_cool() %}
  {{ log("something cool", info=true) }}
{% endmacro %}
```
```yaml
# packages.yml
packages:
  - local: pkg
```
**Before (current v1.6.0b2)**
```
$ dbt run-operation something_cool
13:20:52  Running with dbt=1.6.0-b2
13:20:52  Found 1 model, 0 tests, 0 snapshots, 0 analyses, 314 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups
13:20:52  Encountered an error while running operation: Runtime Error
  dbt could not find a macro with the name "something_cool" in the "my_dbt_project" package
$ dbt run-operation pkg.something_cool
13:20:58  Running with dbt=1.6.0-b2
13:20:59  Found 1 model, 0 tests, 0 snapshots, 0 analyses, 314 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups
13:20:59  something cool
```
**After (fixed with change above)**
```
$ dbt run-operation something_cool
13:16:49  Running with dbt=1.6.0-b2
13:16:49  Found 1 model, 0 tests, 0 snapshots, 0 analyses, 314 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups
13:16:49  something cool
```

Resolving patch:
diff --git a/core/dbt/task/run_operation.py b/core/dbt/task/run_operation.py
--- a/core/dbt/task/run_operation.py
+++ b/core/dbt/task/run_operation.py
@@ -16,6 +16,7 @@
     RunningOperationUncaughtError,
     LogDebugStackTrace,
 )
+from dbt.exceptions import DbtInternalError
 from dbt.node_types import NodeType
 from dbt.task.base import ConfiguredTask
 
@@ -28,14 +29,13 @@ def _get_macro_parts(self):
         if "." in macro_name:
             package_name, macro_name = macro_name.split(".", 1)
         else:
-            package_name = self.config.project_name
+            package_name = None
 
         return package_name, macro_name
 
-    def _run_unsafe(self) -> agate.Table:
+    def _run_unsafe(self, package_name, macro_name) -> agate.Table:
         adapter = get_adapter(self.config)
 
-        package_name, macro_name = self._get_macro_parts()
         macro_kwargs = self.args.args
 
         with adapter.connection_named("macro_{}".format(macro_name)):
@@ -52,8 +52,10 @@ def run(self) -> RunResultsArtifact:
 
         success = True
 
+        package_name, macro_name = self._get_macro_parts()
+
         try:
-            self._run_unsafe()
+            self._run_unsafe(package_name, macro_name)
         except dbt.exceptions.Exception as exc:
             fire_event(RunningOperationCaughtError(exc=str(exc)))
             fire_event(LogDebugStackTrace(exc_info=traceback.format_exc()))
@@ -65,9 +67,19 @@ def run(self) -> RunResultsArtifact:
 
         end = datetime.utcnow()
 
-        package_name, macro_name = self._get_macro_parts()
-        fqn = [NodeType.Operation, package_name, macro_name]
-        unique_id = ".".join(fqn)
+        macro = (
+            self.manifest.find_macro_by_name(macro_name, self.config.project_name, package_name)
+            if self.manifest
+            else None
+        )
+
+        if macro:
+            unique_id = macro.unique_id
+            fqn = unique_id.split(".")
+        else:
+            raise DbtInternalError(
+                f"dbt could not find a macro with the name '{macro_name}' in any package"
+            )
 
         run_result = RunResult(
             adapter_response={},


Test patch:
diff --git a/tests/functional/artifacts/test_artifacts.py b/tests/functional/artifacts/test_artifacts.py
--- a/tests/functional/artifacts/test_artifacts.py
+++ b/tests/functional/artifacts/test_artifacts.py
@@ -690,7 +690,7 @@ def test_run_operation(self, project):
         results, log_output = run_dbt_and_capture(["run-operation", "alter_timezone"])
         assert len(results) == 1
         assert results[0].status == RunStatus.Success
-        assert results[0].unique_id == "operation.test.alter_timezone"
+        assert results[0].unique_id == "macro.test.alter_timezone"
         assert "Timezone set to: America/Los_Angeles" in log_output
 
     def test_run_model_with_operation(self, project):
diff --git a/tests/functional/retry/test_retry.py b/tests/functional/retry/test_retry.py
--- a/tests/functional/retry/test_retry.py
+++ b/tests/functional/retry/test_retry.py
@@ -134,7 +134,7 @@ def test_run_operation(self, project):
         )
 
         expected_statuses = {
-            "operation.test.alter_timezone": RunStatus.Error,
+            "macro.test.alter_timezone": RunStatus.Error,
         }
 
         assert {n.unique_id: n.status for n in results.results} == expected_statuses
diff --git a/tests/functional/run_operations/test_run_operations.py b/tests/functional/run_operations/test_run_operations.py
--- a/tests/functional/run_operations/test_run_operations.py
+++ b/tests/functional/run_operations/test_run_operations.py
@@ -1,8 +1,18 @@
 import os
+
 import pytest
 import yaml
 
-from dbt.tests.util import check_table_does_exist, run_dbt
+from dbt.exceptions import DbtInternalError
+from dbt.tests.util import (
+    check_table_does_exist,
+    run_dbt,
+    write_file,
+    mkdir,
+    run_dbt_and_capture,
+    rm_dir,
+    rm_file,
+)
 from tests.functional.run_operations.fixtures import happy_macros_sql, sad_macros_sql, model_sql
 
 
@@ -66,7 +76,11 @@ def test_macro_exception(self, project):
         self.run_operation("syntax_error", False)
 
     def test_macro_missing(self, project):
-        self.run_operation("this_macro_does_not_exist", False)
+        with pytest.raises(
+            DbtInternalError,
+            match="dbt could not find a macro with the name 'this_macro_does_not_exist' in any package",
+        ):
+            self.run_operation("this_macro_does_not_exist", False)
 
     def test_cannot_connect(self, project):
         self.run_operation("no_args", extra_args=["--target", "noaccess"], expect_pass=False)
@@ -90,3 +104,38 @@ def test_access_graph(self, project):
     def test_print(self, project):
         # Tests that calling the `print()` macro does not cause an exception
         self.run_operation("print_something")
+
+    def test_run_operation_local_macro(self, project):
+        pkg_macro = """
+{% macro something_cool() %}
+    {{ log("something cool", info=true) }}
+{% endmacro %}
+        """
+
+        mkdir("pkg/macros")
+
+        write_file(pkg_macro, "pkg/macros/something_cool.sql")
+
+        pkg_yaml = """
+packages:
+    - local: pkg
+        """
+
+        write_file(pkg_yaml, "packages.yml")
+
+        pkg_dbt_project = """
+name: 'pkg'
+        """
+
+        write_file(pkg_dbt_project, "pkg/dbt_project.yml")
+
+        run_dbt(["deps"])
+
+        results, log_output = run_dbt_and_capture(["run-operation", "something_cool"])
+        assert "something cool" in log_output
+
+        results, log_output = run_dbt_and_capture(["run-operation", "pkg.something_cool"])
+        assert "something cool" in log_output
+
+        rm_dir("pkg")
+        rm_file("packages.yml")