Repository: dbt-labs/dbt-core

Issue:
dbt build: Add to RPC server
Add `dbt build` to the RPC server.  This should be identical to how most other tasks are implemented (`dbt run` would be a good starting place).  

TODO: Find docs on adding tasks to RPC server and link.


Discussion:

Resolving patch:
diff --git a/core/dbt/contracts/rpc.py b/core/dbt/contracts/rpc.py
--- a/core/dbt/contracts/rpc.py
+++ b/core/dbt/contracts/rpc.py
@@ -116,6 +116,16 @@ class RPCDocsGenerateParameters(RPCParameters):
     state: Optional[str] = None
 
 
+@dataclass
+class RPCBuildParameters(RPCParameters):
+    threads: Optional[int] = None
+    models: Union[None, str, List[str]] = None
+    exclude: Union[None, str, List[str]] = None
+    selector: Optional[str] = None
+    state: Optional[str] = None
+    defer: Optional[bool] = None
+
+
 @dataclass
 class RPCCliParameters(RPCParameters):
     cli: str
diff --git a/core/dbt/main.py b/core/dbt/main.py
--- a/core/dbt/main.py
+++ b/core/dbt/main.py
@@ -1072,15 +1072,15 @@ def parse_args(args, cls=DBTArgumentParser):
     seed_sub = _build_seed_subparser(subs, base_subparser)
     # --threads, --no-version-check
     _add_common_arguments(run_sub, compile_sub, generate_sub, test_sub,
-                          rpc_sub, seed_sub, parse_sub)
+                          rpc_sub, seed_sub, parse_sub, build_sub)
     # --models, --exclude
     # list_sub sets up its own arguments.
     _add_selection_arguments(build_sub, run_sub, compile_sub, generate_sub, test_sub)
     _add_selection_arguments(snapshot_sub, seed_sub, models_name='select')
     # --defer
-    _add_defer_argument(run_sub, test_sub)
+    _add_defer_argument(run_sub, test_sub, build_sub)
     # --full-refresh
-    _add_table_mutability_arguments(run_sub, compile_sub)
+    _add_table_mutability_arguments(run_sub, compile_sub, build_sub)
 
     _build_docs_serve_subparser(docs_subs, base_subparser)
     _build_source_freshness_subparser(source_subs, base_subparser)
diff --git a/core/dbt/task/rpc/project_commands.py b/core/dbt/task/rpc/project_commands.py
--- a/core/dbt/task/rpc/project_commands.py
+++ b/core/dbt/task/rpc/project_commands.py
@@ -21,6 +21,7 @@
     RPCSnapshotParameters,
     RPCSourceFreshnessParameters,
     RPCListParameters,
+    RPCBuildParameters,
 )
 from dbt.exceptions import RuntimeException
 from dbt.rpc.method import (
@@ -37,6 +38,7 @@
 from dbt.task.snapshot import SnapshotTask
 from dbt.task.test import TestTask
 from dbt.task.list import ListTask
+from dbt.task.build import BuildTask
 
 from .base import RPCTask
 from .cli import HasCLI
@@ -305,3 +307,22 @@ def output_results(results):
             output=[json.loads(x) for x in results],
             logs=None
         )
+
+
+class RemoteBuildProjectTask(RPCCommandTask[RPCBuildParameters], BuildTask):
+    METHOD_NAME = 'build'
+
+    def set_args(self, params: RPCBuildParameters) -> None:
+        self.args.models = self._listify(params.models)
+        self.args.exclude = self._listify(params.exclude)
+        self.args.selector_name = params.selector
+
+        if params.threads is not None:
+            self.args.threads = params.threads
+        if params.defer is None:
+            self.args.defer = flags.DEFER_MODE
+        else:
+            self.args.defer = params.defer
+
+        self.args.state = state_path(params.state)
+        self.set_previous_state()


Test patch:
diff --git a/test/rpc/test_build.py b/test/rpc/test_build.py
new file mode 100644
--- /dev/null
+++ b/test/rpc/test_build.py
@@ -0,0 +1,136 @@
+import os
+import pytest
+import yaml
+from .util import (
+    assert_has_threads,
+    get_querier,
+    get_write_manifest,
+    ProjectDefinition,
+)
+
+snapshot_data = '''
+{% snapshot my_snapshot %}
+
+    {{
+        config(
+            target_database=database,
+            target_schema=schema,
+            unique_key='id',
+            strategy='timestamp',
+            updated_at='updated_at',
+        )
+    }}
+    select id, cast('2019-10-31 23:59:40' as timestamp) as updated_at
+    from {{ ref('my_model') }}
+
+{% endsnapshot %}
+'''
+
+@pytest.mark.supported('postgres')
+def test_rpc_build_threads(
+    project_root, profiles_root, dbt_profile, unique_schema
+):
+    schema_yaml = {
+        'version': 2,
+        'models': [{
+            'name': 'my_model',
+            'columns': [
+                {
+                    'name': 'id',
+                    'tests': ['not_null', 'unique'],
+                },
+            ],
+        }],
+    }
+    project = ProjectDefinition(
+        project_data={'seeds': {'+quote_columns': False}},
+        models={
+            'my_model.sql': 'select * from {{ ref("data") }}',
+            'schema.yml': yaml.safe_dump(schema_yaml)
+        },
+        seeds={'data.csv': 'id,message\n1,hello\n2,goodbye'},
+        snapshots={'my_snapshots.sql': snapshot_data},
+    )
+    querier_ctx = get_querier(
+        project_def=project,
+        project_dir=project_root,
+        profiles_dir=profiles_root,
+        schema=unique_schema,
+        test_kwargs={},
+    )
+    with querier_ctx as querier:
+        # first run dbt to get the model built
+        querier.async_wait_for_result(querier.build())
+
+        results = querier.async_wait_for_result(querier.test(threads=5))
+        assert_has_threads(results, 5)
+
+        results = querier.async_wait_for_result(
+            querier.cli_args('build --threads=7')
+        )
+        assert_has_threads(results, 7)
+
+
+@pytest.mark.supported('postgres')
+def test_rpc_build_state(
+    project_root, profiles_root, dbt_profile, unique_schema
+):
+    schema_yaml = {
+        'version': 2,
+        'models': [{
+            'name': 'my_model',
+            'columns': [
+                {
+                    'name': 'id',
+                    'tests': ['not_null', 'unique'],
+                },
+            ],
+        }],
+    }
+    project = ProjectDefinition(
+        project_data={'seeds': {'+quote_columns': False}},
+        models={
+            'my_model.sql': 'select * from {{ ref("data") }}',
+            'schema.yml': yaml.safe_dump(schema_yaml)
+        },
+        seeds={'data.csv': 'id,message\n1,hello\n2,goodbye'},
+        snapshots={'my_snapshots.sql': snapshot_data},
+    )
+    querier_ctx = get_querier(
+        project_def=project,
+        project_dir=project_root,
+        profiles_dir=profiles_root,
+        schema=unique_schema,
+        test_kwargs={},
+    )
+    with querier_ctx as querier:
+        state_dir = os.path.join(project_root, 'state')
+        os.makedirs(state_dir)
+
+        results = querier.async_wait_for_result(querier.build())
+        assert len(results['results']) == 5
+
+        get_write_manifest(querier, os.path.join(state_dir, 'manifest.json'))
+
+        project.models['my_model.sql'] = 'select * from {{ ref("data" )}} where id = 2'
+        project.write_models(project_root, remove=True)
+        querier.sighup()
+        assert querier.wait_for_status('ready') is True
+
+        results = querier.async_wait_for_result(
+            querier.build(state='./state', models=['state:modified'])
+        )
+        assert len(results['results']) == 3
+
+        get_write_manifest(querier, os.path.join(state_dir, 'manifest.json'))
+
+        results = querier.async_wait_for_result(
+            querier.build(state='./state', models=['state:modified']),
+        )
+        assert len(results['results']) == 0
+        
+        # a better test of defer would require multiple targets
+        results = querier.async_wait_for_result(
+            querier.build(state='./state', models=['state:modified'], defer=True)
+        )
+        assert len(results['results']) == 0
diff --git a/test/rpc/test_seed.py b/test/rpc/test_seed.py
--- a/test/rpc/test_seed.py
+++ b/test/rpc/test_seed.py
@@ -13,7 +13,7 @@ def test_rpc_seed_threads(
     project_root, profiles_root, dbt_profile, unique_schema
 ):
     project = ProjectDefinition(
-        project_data={'seeds': {'config': {'quote_columns': False}}},
+        project_data={'seeds': {'+quote_columns': False}},
         seeds={'data.csv': 'a,b\n1,hello\n2,goodbye'},
     )
     querier_ctx = get_querier(
@@ -39,7 +39,7 @@ def test_rpc_seed_include_exclude(
     project_root, profiles_root, dbt_profile, unique_schema
 ):
     project = ProjectDefinition(
-        project_data={'seeds': {'config': {'quote_columns': False}}},
+        project_data={'seeds': {'+quote_columns': False}},
         seeds={
             'data_1.csv': 'a,b\n1,hello\n2,goodbye',
             'data_2.csv': 'a,b\n1,data',
diff --git a/test/rpc/util.py b/test/rpc/util.py
--- a/test/rpc/util.py
+++ b/test/rpc/util.py
@@ -379,6 +379,30 @@ def test(
             method='test', params=params, request_id=request_id
         )
 
+    def build(
+        self,
+        models: Optional[Union[str, List[str]]] = None,
+        exclude: Optional[Union[str, List[str]]] = None,
+        threads: Optional[int] = None,
+        request_id: int = 1,
+        defer: Optional[bool] = None,
+        state: Optional[bool] = None,
+    ):
+        params = {}
+        if models is not None:
+            params['models'] = models
+        if exclude is not None:
+            params['exclude'] = exclude
+        if threads is not None:
+            params['threads'] = threads
+        if defer is not None:
+            params['defer'] = defer
+        if state is not None:
+            params['state'] = state
+        return self.request(
+            method='build', params=params, request_id=request_id
+        )
+
     def docs_generate(self, compile: bool = None, request_id: int = 1):
         params = {}
         if compile is not None: