Repository: dbt-labs/dbt-core (python)

Issue:
[CT-2699] [Feature] "interactive" compile should include the compiled code of a snapshot

### Is this a new bug in dbt-core?

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

### Current Behavior

With dbt 1.5 the `compile` command was extended to allow "interactive" compilation of an arbitrary node by using syntax similar to :
`dbt compile --select name_of_node`

In our testing we found this seems to work for all types of node, including **snapshots**.

However, the vanilla operation of `dbt compile` does not support snapshots. The [documentation](https://docs.getdbt.com/reference/commands/compile) states "dbt compile generates executable SQL from source `model`, `test`, and `analysis` files." i.e. it does not include snapshots and indeed if `dbt compile` is run for a project no code files are created for snapshots in "/target/compiled/..".

This has created an inconsistency where one usage of `compile` will generate output for a snapshot but another will not. 

This is not a bug as such but I think the inconsistent behaviour is potentially confusing for users, and it's not clear why standard `compile` doesn't already work for snapshots since it is apparently possible based on the interactive version.  

### Expected Behavior

All usages of `dbt compile` should produce compiled code for snapshots i.e. non-interactive uses of `compile` should generate the same compiled code as interactive use.

### Steps To Reproduce

1. Create a snapshot file
2. Run `dbt compile`
3. Note no output produced under "/target/compiled/"
4. Run `dbt compile --select <name_of_snapshot>`
5. Note compiled code output to command line

### Relevant log output

_No response_

### Environment

```markdown
- OS: Windows
- Python: 3.10.10
- dbt: 1.5.0
```


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

bigquery

### Additional Context

_No response_


Patch:
diff --git a/.changes/unreleased/Features-20260302-123311.yaml b/.changes/unreleased/Features-20260302-123311.yaml
new file mode 100644
index 00000000000..a27f5fc2b79
--- /dev/null
+++ b/.changes/unreleased/Features-20260302-123311.yaml
@@ -0,0 +1,6 @@
+kind: Features
+body: Write compiled SQL for snapshots to target/compiled/ during dbt compile
+time: 2026-03-02T12:33:11.456829+05:30
+custom:
+    Author: aahel
+    Issue: "7867"
diff --git a/core/dbt/compilation.py b/core/dbt/compilation.py
index 35a2b0f288b..3492da39122 100644
--- a/core/dbt/compilation.py
+++ b/core/dbt/compilation.py
@@ -740,10 +740,7 @@ def write_graph_file(self, linker: Linker, manifest: Manifest):
     def _write_node(
         self, node: ManifestSQLNode, split_suffix: Optional[str] = None
     ) -> ManifestSQLNode:
-        if not node.extra_ctes_injected or node.resource_type in (
-            NodeType.Snapshot,
-            NodeType.Seed,
-        ):
+        if not node.extra_ctes_injected or node.resource_type in (NodeType.Seed,):
             return node
         fire_event(WritingInjectedSQLForNode(node_info=get_node_info()))
 
diff --git a/core/dbt/parser/schemas.py b/core/dbt/parser/schemas.py
index 0f0cd162739..f692fea55c2 100644
--- a/core/dbt/parser/schemas.py
+++ b/core/dbt/parser/schemas.py
@@ -1,5 +1,4 @@
 import datetime
-import pathlib
 import re
 import time
 from abc import ABCMeta, abstractmethod
@@ -343,11 +342,7 @@ def _add_yaml_snapshot_nodes_to_manifest(
                 fqn = parser.get_fqn_prefix(block.path.relative_path)
                 fqn.append(snapshot["name"])
 
-                compiled_path = str(
-                    pathlib.PurePath("").joinpath(
-                        block.path.relative_path, snapshot["name"] + ".sql"
-                    )
-                )
+                compiled_path = snapshot["name"] + ".sql"
                 snapshot_node = parser._create_parsetime_node(
                     block,
                     compiled_path,
diff --git a/core/dbt/parser/snapshots.py b/core/dbt/parser/snapshots.py
index 3f9a7326c74..c662cbf70c1 100644
--- a/core/dbt/parser/snapshots.py
+++ b/core/dbt/parser/snapshots.py
@@ -1,6 +1,7 @@
 import os
 from typing import List
 
+from dbt.context.context_config import ContextConfig
 from dbt.contracts.graph.nodes import SnapshotNode
 from dbt.node_types import NodeType
 from dbt.parser.base import SQLParser
@@ -20,7 +21,7 @@ def resource_type(self) -> NodeType:
 
     @classmethod
     def get_compiled_path(cls, block: FileBlock):
-        return block.path.relative_path
+        return block.name + ".sql"
 
     def get_fqn(self, path: str, name: str) -> List[str]:
         """Get the FQN for the node. This impacts node selection and config
@@ -34,6 +35,26 @@ def get_fqn(self, path: str, name: str) -> List[str]:
         fqn.append(name)
         return fqn
 
+    def parse_node(self, block: FileBlock) -> SnapshotNode:
+        compiled_path: str = self.get_compiled_path(block)
+        # Use the file's relative_path for FQN computation (not the compiled_path)
+        # to maintain backward compatibility. The compiled_path is based on the
+        # snapshot name for uniqueness (multiple snapshots can share one file),
+        # but the FQN should still reflect the file structure.
+        fqn = self.get_fqn(block.path.relative_path, block.name)
+
+        config: ContextConfig = self.initial_config(fqn)
+
+        node = self._create_parsetime_node(
+            block=block,
+            path=compiled_path,
+            config=config,
+            fqn=fqn,
+        )
+        self.render_update(node, config)
+        self.add_result_node(block, node)
+        return node
+
     def parse_file(self, file_block: FileBlock) -> None:
         blocks = BlockSearcher(
             source=[file_block],


Test patch:
diff --git a/tests/functional/artifacts/expected_manifest.py b/tests/functional/artifacts/expected_manifest.py
index 1afb19f8838..006d242eeb0 100644
--- a/tests/functional/artifacts/expected_manifest.py
+++ b/tests/functional/artifacts/expected_manifest.py
@@ -684,7 +684,9 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False):
             },
             "snapshot.test.snapshot_seed": {
                 "alias": "snapshot_seed",
-                "compiled_path": None,
+                "compiled_path": os.path.join(
+                    "target", "compiled", "test", "snapshots", "snapshot_seed.sql"
+                ),
                 "build_path": None,
                 "created_at": ANY,
                 "checksum": checksum_file(snapshot_path),
@@ -1374,7 +1376,9 @@ def expected_references_manifest(project):
             },
             "snapshot.test.snapshot_seed": {
                 "alias": "snapshot_seed",
-                "compiled_path": None,
+                "compiled_path": os.path.join(
+                    "target", "compiled", "test", "snapshots", "snapshot_seed.sql"
+                ),
                 "build_path": None,
                 "created_at": ANY,
                 "checksum": checksum_file(snapshot_path),
diff --git a/tests/functional/snapshots/test_basic_snapshot.py b/tests/functional/snapshots/test_basic_snapshot.py
index df01bd2f5dd..c8fe0d2deee 100644
--- a/tests/functional/snapshots/test_basic_snapshot.py
+++ b/tests/functional/snapshots/test_basic_snapshot.py
@@ -405,6 +405,27 @@ def test_basic_snapshot_yaml(self, project):
         snapshot_setup(project, num_snapshot_models=2)
 
 
+class TestYamlSnapshotCompiledPath(BasicYaml):
+    def test_compiled_path(self, project):
+        """Verify yml-based snapshots get correct compiled paths without doubling."""
+        manifest = run_dbt(["parse"])
+        node = manifest.nodes["snapshot.test.snapshot_actual"]
+        assert node.path == "snapshot_actual.sql"
+        assert node.original_file_path == os.path.join("snapshots", "snapshot.yml")
+
+        run_dbt(["compile"])
+        expected = os.path.join(
+            project.project_root,
+            "target",
+            "compiled",
+            "test",
+            "snapshots",
+            "snapshot.yml",
+            "snapshot_actual.sql",
+        )
+        assert os.path.exists(expected), f"Compiled file not found: {expected}"
+
+
 class TestYamlSnapshotPartialParsing(BasicYaml):
     def test_snapshot_partial_parsing(self, project):
         manifest = run_dbt(["parse"])
diff --git a/tests/unit/parser/test_parser.py b/tests/unit/parser/test_parser.py
index b100930385e..ec26d85aa06 100644
--- a/tests/unit/parser/test_parser.py
+++ b/tests/unit/parser/test_parser.py
@@ -1828,7 +1828,7 @@ def test_single_block(self):
                 unique_key="id",
                 materialized="snapshot",
             ),
-            path=normalize("nested/snap_1.sql"),
+            path="foo.sql",
             language="sql",
             raw_code=raw_code,
             checksum=block.file.checksum,
@@ -1899,7 +1899,7 @@ def test_multi_block(self):
                 unique_key="id",
                 materialized="snapshot",
             ),
-            path=normalize("nested/snap_1.sql"),
+            path="foo.sql",
             language="sql",
             raw_code=raw_1,
             checksum=block.file.checksum,
@@ -1938,7 +1938,7 @@ def test_multi_block(self):
                 unique_key="id",
                 materialized="snapshot",
             ),
-            path=normalize("nested/snap_1.sql"),
+            path="bar.sql",
             language="sql",
             raw_code=raw_2,
             checksum=block.file.checksum,