Repository: dbt-labs/dbt-core

Issue:
[Feature] dbt snapshot --defer
### Is there an existing feature request for this?

- [X] I have searched the existing issues

### Describe the Feature

Implement "dbt snapshot --defer", which would behave similar to "dbt run --defer".

### Describe alternatives you've considered

_No response_

### Who will this benefit?

When trying to set up "slim ci", one step in my case would be to run snapshots based on other dbt models.

`dbt snapshot --select 'dbt.*,state:modified' --state manifests/staging`

When running this command, generated references point to current schema, which doesn't have these tables, as they weren't generated during preceding run command:
`dbt run --models state:modified --defer --state manifests/airflow/staging`

### Are you interested in contributing this feature?

_No response_

### Anything else?

https://docs.getdbt.com/reference/node-selection/defer
https://docs.getdbt.com/docs/guides/best-practices#run-only-modified-models-to-test-changes-slim-ci

dbt version:  0.20.1


Discussion:
@iljau Thanks for opening!

The steps here are quite straightforward. I'm going to mark this a `good first issue`, and I'd welcome a PR from a community member.

1. Include snapshot in the subcommands that get the `--defer` flag:
https://github.com/dbt-labs/dbt-core/blob/f79a968a0908add9de1b6acfdb75a6b205134ebd/core/dbt/main.py#L1091-L1092

2. Remove `defer_to_manifest` override in `SnapshotTask`, so as to inherit the implementation from `RunTask`:
https://github.com/dbt-labs/dbt-core/blob/f79a968a0908add9de1b6acfdb75a6b205134ebd/core/dbt/task/snapshot.py#L25-L27

3. Then, we'd want to add/adjust a test case combining snapshots + deferral:
https://github.com/dbt-labs/dbt-core/blob/f79a968a0908add9de1b6acfdb75a6b205134ebd/test/integration/062_defer_state_test/test_defer_state.py#L154-L161

Is this a change you'd be interested in contributing?

### one step further

At this point, there's no good reason not to support `--defer` for `compile`, too, and thereby `docs generate` as well (https://github.com/dbt-labs/dbt-core/issues/3741). (The only reason I can come up with: we might eventually want to rename/rework the UX of deferral (#2968)).

For that support:
- Add the `--defer` flag to the `compile` and `docs` subparsers, too
- Move `defer_to_manifest` up from `RunTask` into [`CompileTask`](https://github.com/dbt-labs/dbt-core/blob/f79a968a0908add9de1b6acfdb75a6b205134ebd/core/dbt/task/compile.py#L36) (its parent)

### alternative

The `build` task, new in v0.21, supports both snapshots and deferral:
```
$ dbt build --resource-type snapshot --defer --state manifests/airflow/staging`
```

So, that's something you can use in the meantime.
Hello @jtcohen6 @iljau  :wave:,

Thank you for opening this issue and providing a step by step solution :slightly_smiling_face:.
Happy to work on this feature and to raise a PR for it. Ok for you?

:pray: 
I switched to using `dbt build` and don't have immediate plans to work on a fix.

Resolving patch:
diff --git a/core/dbt/main.py b/core/dbt/main.py
--- a/core/dbt/main.py
+++ b/core/dbt/main.py
@@ -1109,7 +1109,7 @@ def parse_args(args, cls=DBTArgumentParser):
     _add_selection_arguments(
         run_sub, compile_sub, generate_sub, test_sub, snapshot_sub, seed_sub)
     # --defer
-    _add_defer_argument(run_sub, test_sub, build_sub)
+    _add_defer_argument(run_sub, test_sub, build_sub, snapshot_sub)
     # --full-refresh
     _add_table_mutability_arguments(run_sub, compile_sub, build_sub)
 
diff --git a/core/dbt/task/snapshot.py b/core/dbt/task/snapshot.py
--- a/core/dbt/task/snapshot.py
+++ b/core/dbt/task/snapshot.py
@@ -43,10 +43,6 @@ class SnapshotTask(RunTask):
     def raise_on_first_error(self):
         return False
 
-    def defer_to_manifest(self, adapter, selected_uids):
-        # snapshots don't defer
-        return
-
     def get_node_selector(self):
         if self.manifest is None or self.graph is None:
             raise InternalException(


Test patch:
diff --git a/test/integration/062_defer_state_test/test_defer_state.py b/test/integration/062_defer_state_test/test_defer_state.py
--- a/test/integration/062_defer_state_test/test_defer_state.py
+++ b/test/integration/062_defer_state_test/test_defer_state.py
@@ -5,6 +5,7 @@
 import shutil
 
 import pytest
+import dbt.exceptions
 
 
 class TestDeferState(DBTIntegrationTest):
@@ -46,6 +47,29 @@ def copy_state(self):
         os.makedirs('state')
         shutil.copyfile('target/manifest.json', 'state/manifest.json')
 
+    def run_and_snapshot_defer(self):
+        results = self.run_dbt(['seed'])
+        assert len(results) == 1
+        assert not any(r.node.deferred for r in results)
+        results = self.run_dbt(['run'])
+        assert len(results) == 2
+        assert not any(r.node.deferred for r in results)
+        results = self.run_dbt(['test'])
+        assert len(results) == 2
+
+        # snapshot succeeds without --defer
+        results = self.run_dbt(['snapshot'])
+
+        # no state, snapshot fails
+        with pytest.raises(dbt.exceptions.RuntimeException):
+            results = self.run_dbt(['snapshot', '--state', 'state', '--defer'])
+
+        # copy files
+        self.copy_state()
+
+        # defer test, it succeeds
+        results = self.run_dbt(['snapshot', '--state', 'state', '--defer'])
+
     def run_and_defer(self):
         results = self.run_dbt(['seed'])
         assert len(results) == 1
@@ -151,15 +175,11 @@ def run_defer_deleted_upstream(self):
     @use_profile('postgres')
     def test_postgres_state_changetarget(self):
         self.run_and_defer()
-        # these should work without --defer!
-        self.run_dbt(['snapshot'])
+
         # make sure these commands don't work with --defer
         with pytest.raises(SystemExit):
             self.run_dbt(['seed', '--defer'])
 
-        with pytest.raises(SystemExit):
-            self.run_dbt(['snapshot', '--defer'])
-
     @use_profile('postgres')
     def test_postgres_state_changedir(self):
         self.run_switchdirs_defer()
@@ -171,3 +191,7 @@ def test_postgres_state_defer_iffnotexists(self):
     @use_profile('postgres')
     def test_postgres_state_defer_deleted_upstream(self):
         self.run_defer_deleted_upstream()
+
+    @use_profile('postgres')
+    def test_postgres_state_snapshot_defer(self):
+        self.run_and_snapshot_defer()