Repository: dbt-labs/dbt-core

Issue:
[CT-312] Convert dbt-adapters-tests
Converting the https://github.com/dbt-labs/dbt-adapter-tests tests to from yaml 


Discussion:

Resolving patch:
diff --git a/core/dbt/contracts/graph/model_config.py b/core/dbt/contracts/graph/model_config.py
--- a/core/dbt/contracts/graph/model_config.py
+++ b/core/dbt/contracts/graph/model_config.py
@@ -485,7 +485,8 @@ class SnapshotConfig(EmptySnapshotConfig):
     target_schema: Optional[str] = None
     target_database: Optional[str] = None
     updated_at: Optional[str] = None
-    check_cols: Optional[Union[str, List[str]]] = None
+    # Not using Optional because of serialization issues with a Union of str and List[str]
+    check_cols: Union[str, List[str], None] = None
 
     @classmethod
     def validate(cls, data):


Test patch:
diff --git a/core/dbt/tests/adapter.py b/core/dbt/tests/adapter.py
new file mode 100644
--- /dev/null
+++ b/core/dbt/tests/adapter.py
@@ -0,0 +1,212 @@
+from itertools import chain, repeat
+from dbt.context import providers
+from unittest.mock import patch
+
+# These functions were extracted from the dbt-adapter-tests spec_file.py.
+# They are used in the 'adapter' tests directory. At some point they
+# might be moved to dbts.tests.util if they are of general purpose use,
+# but leaving here for now to keep the adapter work more contained.
+# We may want to consolidate in the future since some of this is kind
+# of duplicative of the functionality in dbt.tests.tables.
+
+
+class TestProcessingException(Exception):
+    pass
+
+
+def relation_from_name(adapter, name: str):
+    """reverse-engineer a relation (including quoting) from a given name and
+    the adapter. Assumes that relations are split by the '.' character.
+    """
+
+    # Different adapters have different Relation classes
+    cls = adapter.Relation
+    credentials = adapter.config.credentials
+    quote_policy = cls.get_default_quote_policy().to_dict()
+    include_policy = cls.get_default_include_policy().to_dict()
+    kwargs = {}  # This will contain database, schema, identifier
+
+    parts = name.split(".")
+    names = ["database", "schema", "identifier"]
+    defaults = [credentials.database, credentials.schema, None]
+    values = chain(repeat(None, 3 - len(parts)), parts)
+    for name, value, default in zip(names, values, defaults):
+        # no quote policy -> use the default
+        if value is None:
+            if default is None:
+                include_policy[name] = False
+            value = default
+        else:
+            include_policy[name] = True
+            # if we have a value, we can figure out the quote policy.
+            trimmed = value[1:-1]
+            if adapter.quote(trimmed) == value:
+                quote_policy[name] = True
+                value = trimmed
+            else:
+                quote_policy[name] = False
+        kwargs[name] = value
+
+    relation = cls.create(
+        include_policy=include_policy,
+        quote_policy=quote_policy,
+        **kwargs,
+    )
+    return relation
+
+
+def check_relation_types(adapter, relation_to_type):
+    """
+    Relation name to table/view
+    {
+        "base": "table",
+        "other": "view",
+    }
+    """
+
+    expected_relation_values = {}
+    found_relations = []
+    schemas = set()
+
+    for key, value in relation_to_type.items():
+        relation = relation_from_name(adapter, key)
+        expected_relation_values[relation] = value
+        schemas.add(relation.without_identifier())
+
+    with patch.object(providers, "get_adapter", return_value=adapter):
+        with adapter.connection_named("__test"):
+            for schema in schemas:
+                found_relations.extend(adapter.list_relations_without_caching(schema))
+
+    for key, value in relation_to_type.items():
+        for relation in found_relations:
+            # this might be too broad
+            if relation.identifier == key:
+                assert relation.type == value, (
+                    f"Got an unexpected relation type of {relation.type} "
+                    f"for relation {key}, expected {value}"
+                )
+
+
+def check_relations_equal(adapter, relation_names):
+    if len(relation_names) < 2:
+        raise TestProcessingException(
+            "Not enough relations to compare",
+        )
+    relations = [relation_from_name(adapter, name) for name in relation_names]
+
+    with patch.object(providers, "get_adapter", return_value=adapter):
+        with adapter.connection_named("_test"):
+            basis, compares = relations[0], relations[1:]
+            columns = [c.name for c in adapter.get_columns_in_relation(basis)]
+
+            for relation in compares:
+                sql = adapter.get_rows_different_sql(basis, relation, column_names=columns)
+                _, tbl = adapter.execute(sql, fetch=True)
+                num_rows = len(tbl)
+                assert (
+                    num_rows == 1
+                ), f"Invalid sql query from get_rows_different_sql: incorrect number of rows ({num_rows})"
+                num_cols = len(tbl[0])
+                assert (
+                    num_cols == 2
+                ), f"Invalid sql query from get_rows_different_sql: incorrect number of cols ({num_cols})"
+                row_count_difference = tbl[0][0]
+                assert (
+                    row_count_difference == 0
+                ), f"Got {row_count_difference} difference in row count betwen {basis} and {relation}"
+                rows_mismatched = tbl[0][1]
+                assert (
+                    rows_mismatched == 0
+                ), f"Got {rows_mismatched} different rows between {basis} and {relation}"
+
+
+def get_unique_ids_in_results(results):
+    unique_ids = []
+    for result in results:
+        unique_ids.append(result.node.unique_id)
+    return unique_ids
+
+
+def check_result_nodes_by_name(results, names):
+    result_names = []
+    for result in results:
+        result_names.append(result.node.name)
+    assert set(names) == set(result_names)
+
+
+def check_result_nodes_by_unique_id(results, unique_ids):
+    result_unique_ids = []
+    for result in results:
+        result_unique_ids.append(result.node.unique_id)
+    assert set(unique_ids) == set(result_unique_ids)
+
+
+def update_rows(adapter, update_rows_config):
+    """
+    {
+      "name": "base",
+      "dst_col": "some_date"
+      "clause": {
+          "type": "add_timestamp",
+          "src_col": "some_date",
+       "where" "id > 10"
+    }
+    """
+    for key in ["name", "dst_col", "clause"]:
+        if key not in update_rows_config:
+            raise TestProcessingException(f"Invalid update_rows: no {key}")
+
+    clause = update_rows_config["clause"]
+    clause = generate_update_clause(adapter, clause)
+
+    where = None
+    if "where" in update_rows_config:
+        where = update_rows_config["where"]
+
+    name = update_rows_config["name"]
+    dst_col = update_rows_config["dst_col"]
+    relation = relation_from_name(adapter, name)
+
+    with patch.object(providers, "get_adapter", return_value=adapter):
+        with adapter.connection_named("_test"):
+            sql = adapter.update_column_sql(
+                dst_name=str(relation),
+                dst_column=dst_col,
+                clause=clause,
+                where_clause=where,
+            )
+            print(f"--- update_rows sql: {sql}")
+            adapter.execute(sql, auto_begin=True)
+            adapter.commit_if_has_connection()
+
+
+def generate_update_clause(adapter, clause) -> str:
+    """
+    Called by update_rows function. Expects the "clause" dictionary
+    documented in 'update_rows.
+    """
+
+    if "type" not in clause or clause["type"] not in ["add_timestamp", "add_string"]:
+        raise TestProcessingException("invalid update_rows clause: type missing or incorrect")
+    clause_type = clause["type"]
+
+    if clause_type == "add_timestamp":
+        if "src_col" not in clause:
+            raise TestProcessingException("Invalid update_rows clause: no src_col")
+        add_to = clause["src_col"]
+        kwargs = {k: v for k, v in clause.items() if k in ("interval", "number")}
+        with patch.object(providers, "get_adapter", return_value=adapter):
+            with adapter.connection_named("_test"):
+                return adapter.timestamp_add_sql(add_to=add_to, **kwargs)
+    elif clause_type == "add_string":
+        for key in ["src_col", "value"]:
+            if key not in clause:
+                raise TestProcessingException(f"Invalid update_rows clause: no {key}")
+        src_col = clause["src_col"]
+        value = clause["value"]
+        location = clause.get("location", "append")
+        with patch.object(providers, "get_adapter", return_value=adapter):
+            with adapter.connection_named("_test"):
+                return adapter.string_add_sql(src_col, value, location)
+    return ""
diff --git a/core/dbt/tests/fixtures/project.py b/core/dbt/tests/fixtures/project.py
--- a/core/dbt/tests/fixtures/project.py
+++ b/core/dbt/tests/fixtures/project.py
@@ -4,17 +4,12 @@
 from argparse import Namespace
 from datetime import datetime
 import yaml
-from unittest.mock import patch
-from contextlib import contextmanager
 
 import dbt.flags as flags
 from dbt.config.runtime import RuntimeConfig
 from dbt.adapters.factory import get_adapter, register_adapter, reset_adapters
 from dbt.events.functions import setup_event_logger
-from dbt.context import providers
-from dbt.events.functions import fire_event
-from dbt.events.test_types import IntegrationTestDebug
-from dbt.tests.util import write_file
+from dbt.tests.util import write_file, run_sql_with_adapter
 
 
 # These are the fixtures that are used in dbt core functional tests
@@ -288,60 +283,16 @@ def __init__(
         self.test_schema = test_schema
         self.database = database
 
-    @contextmanager
-    def get_connection(self, name="__test"):
-        """Since the 'adapter' in dbt.adapters.factory may have been replaced by execution
-        of dbt commands since the test 'adapter' was created, we patch the 'get_adapter' call in
-        dbt.context.providers, so that macros that are called refer to this test adapter.
-        This allows tests to run normal adapter macros as if reset_adapters() were not
-        called by handle_and_check (for asserts, etc).
-        """
-        with patch.object(providers, "get_adapter", return_value=self.adapter):
-            with self.adapter.connection_named(name):
-                conn = self.adapter.connections.get_thread_connection()
-                yield conn
-
     # Run sql from a path
-    def run_sql_file(self, sql_path):
+    def run_sql_file(self, sql_path, fetch=None):
         with open(sql_path, "r") as f:
             statements = f.read().split(";")
             for statement in statements:
-                self.run_sql(statement)
+                self.run_sql(statement, fetch)
 
     # run sql from a string, using adapter saved at test startup
     def run_sql(self, sql, fetch=None):
-        if sql.strip() == "":
-            return
-        # substitute schema and database in sql
-        adapter = self.adapter
-        kwargs = {
-            "schema": self.test_schema,
-            "database": adapter.quote(self.database),
-        }
-        sql = sql.format(**kwargs)
-
-        with self.get_connection("__test") as conn:
-            msg = f'test connection "{conn.name}" executing: {sql}'
-            fire_event(IntegrationTestDebug(msg=msg))
-            with conn.handle.cursor() as cursor:
-                try:
-                    cursor.execute(sql)
-                    conn.handle.commit()
-                    conn.handle.commit()
-                    if fetch == "one":
-                        return cursor.fetchone()
-                    elif fetch == "all":
-                        return cursor.fetchall()
-                    else:
-                        return
-                except BaseException as e:
-                    if conn.handle and not getattr(conn.handle, "closed", True):
-                        conn.handle.rollback()
-                    print(sql)
-                    print(e)
-                    raise
-                finally:
-                    conn.transaction_open = False
+        return run_sql_with_adapter(self.adapter, sql, fetch=fetch)
 
     def get_tables_in_schema(self):
         sql = """
diff --git a/core/dbt/tests/util.py b/core/dbt/tests/util.py
--- a/core/dbt/tests/util.py
+++ b/core/dbt/tests/util.py
@@ -1,11 +1,16 @@
 import os
 import shutil
+import yaml
+import json
 from typing import List
 
 from dbt.main import handle_and_check
 from dbt.logger import log_manager
 from dbt.contracts.graph.manifest import Manifest
-from dbt.events.functions import capture_stdout_logs, stop_capture_stdout_logs
+from dbt.events.functions import fire_event, capture_stdout_logs, stop_capture_stdout_logs
+from dbt.events.test_types import IntegrationTestDebug
+from dbt.context import providers
+from unittest.mock import patch
 
 
 # This is used in pytest tests to run dbt
@@ -85,3 +90,59 @@ def read_file(*paths):
     with open(os.path.join(*paths), "r") as fp:
         contents = fp.read()
     return contents
+
+
+def get_artifact(*paths):
+    contents = read_file(*paths)
+    dct = json.loads(contents)
+    return dct
+
+
+# For updating yaml config files
+def update_config_file(updates, *paths):
+    current_yaml = read_file(*paths)
+    config = yaml.safe_load(current_yaml)
+    config.update(updates)
+    new_yaml = yaml.safe_dump(config)
+    write_file(new_yaml, *paths)
+
+
+def run_sql_with_adapter(adapter, sql, fetch=None):
+    if sql.strip() == "":
+        return
+    # substitute schema and database in sql
+    kwargs = {
+        "schema": adapter.config.credentials.schema,
+        "database": adapter.quote(adapter.config.credentials.database),
+    }
+    sql = sql.format(**kwargs)
+
+    # Since the 'adapter' in dbt.adapters.factory may have been replaced by execution
+    # of dbt commands since the test 'adapter' was created, we patch the 'get_adapter' call in
+    # dbt.context.providers, so that macros that are called refer to this test adapter.
+    # This allows tests to run normal adapter macros as if reset_adapters() were not
+    # called by handle_and_check (for asserts, etc).
+    with patch.object(providers, "get_adapter", return_value=adapter):
+        with adapter.connection_named("__test"):
+            conn = adapter.connections.get_thread_connection()
+            msg = f'test connection "{conn.name}" executing: {sql}'
+            fire_event(IntegrationTestDebug(msg=msg))
+            with conn.handle.cursor() as cursor:
+                try:
+                    cursor.execute(sql)
+                    conn.handle.commit()
+                    conn.handle.commit()
+                    if fetch == "one":
+                        return cursor.fetchone()
+                    elif fetch == "all":
+                        return cursor.fetchall()
+                    else:
+                        return
+                except BaseException as e:
+                    if conn.handle and not getattr(conn.handle, "closed", True):
+                        conn.handle.rollback()
+                    print(sql)
+                    print(e)
+                    raise
+                finally:
+                    conn.transaction_open = False
diff --git a/tests/fixtures/jaffle_shop.py b/tests/fixtures/jaffle_shop.py
--- a/tests/fixtures/jaffle_shop.py
+++ b/tests/fixtures/jaffle_shop.py
@@ -372,44 +372,43 @@
 """
 
 
-@pytest.fixture
-def models():
-    return {
-        "customers.sql": customers_sql,
-        "docs.md": docs_md,
-        "orders.sql": orders_sql,
-        "overview.md": overview_md,
-        "schema.yml": schema_yml,
-        "staging": {
-            "schema.yml": staging_schema_yml,
-            "stg_customers.sql": staging_stg_customers_sql,
-            "stg_orders.sql": staging_stg_orders_sql,
-            "stg_payments.sql": staging_stg_payments_sql,
-        },
-    }
-
-
-@pytest.fixture
-def seeds():
-    # Read seed file and return
-    seeds = {}
-    dir_path = os.path.dirname(os.path.realpath(__file__))
-    for file_name in ("raw_customers.csv", "raw_orders.csv", "raw_payments.csv"):
-        contents = read_file(dir_path, "jaffle_shop_data", file_name)
-        seeds[file_name] = contents
-    return seeds
-
-
-@pytest.fixture
-def project_config_update():
-    return {
-        "name": "jaffle_shop",
-        "models": {
-            "jaffle_shop": {
-                "materialized": "table",
-                "staging": {
-                    "materialized": "view",
-                },
-            }
-        },
-    }
+class JaffleShopProject:
+    @pytest.fixture
+    def models(self):
+        return {
+            "customers.sql": customers_sql,
+            "docs.md": docs_md,
+            "orders.sql": orders_sql,
+            "overview.md": overview_md,
+            "schema.yml": schema_yml,
+            "staging": {
+                "schema.yml": staging_schema_yml,
+                "stg_customers.sql": staging_stg_customers_sql,
+                "stg_orders.sql": staging_stg_orders_sql,
+                "stg_payments.sql": staging_stg_payments_sql,
+            },
+        }
+
+    @pytest.fixture
+    def seeds(self):
+        # Read seed file and return
+        seeds = {}
+        dir_path = os.path.dirname(os.path.realpath(__file__))
+        for file_name in ("raw_customers.csv", "raw_orders.csv", "raw_payments.csv"):
+            contents = read_file(dir_path, "jaffle_shop_data", file_name)
+            seeds[file_name] = contents
+        return seeds
+
+    @pytest.fixture
+    def project_config_update(self):
+        return {
+            "name": "jaffle_shop",
+            "models": {
+                "jaffle_shop": {
+                    "materialized": "table",
+                    "staging": {
+                        "materialized": "view",
+                    },
+                }
+            },
+        }
diff --git a/tests/functional/adapter/__init__.py b/tests/functional/adapter/__init__.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/__init__.py
@@ -0,0 +1 @@
+# Adapter tests
diff --git a/tests/functional/adapter/files.py b/tests/functional/adapter/files.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/files.py
@@ -0,0 +1,208 @@
+seeds_base_csv = """
+id,name,some_date
+1,Easton,1981-05-20T06:46:51
+2,Lillian,1978-09-03T18:10:33
+3,Jeremiah,1982-03-11T03:59:51
+4,Nolan,1976-05-06T20:21:35
+5,Hannah,1982-06-23T05:41:26
+6,Eleanor,1991-08-10T23:12:21
+7,Lily,1971-03-29T14:58:02
+8,Jonathan,1988-02-26T02:55:24
+9,Adrian,1994-02-09T13:14:23
+10,Nora,1976-03-01T16:51:39
+""".lstrip()
+
+
+seeds_added_csv = (
+    seeds_base_csv
+    + """
+11,Mateo,2014-09-07T17:04:27
+12,Julian,2000-02-04T11:48:30
+13,Gabriel,2001-07-10T07:32:52
+14,Isaac,2002-11-24T03:22:28
+15,Levi,2009-11-15T11:57:15
+16,Elizabeth,2005-04-09T03:50:11
+17,Grayson,2019-08-06T19:28:17
+18,Dylan,2014-03-01T11:50:41
+19,Jayden,2009-06-06T07:12:49
+20,Luke,2003-12-05T21:42:18
+""".lstrip()
+)
+
+seeds_newcolumns_csv = """
+id,name,some_date,last_initial
+1,Easton,1981-05-20T06:46:51,A
+2,Lillian,1978-09-03T18:10:33,B
+3,Jeremiah,1982-03-11T03:59:51,C
+4,Nolan,1976-05-06T20:21:35,D
+5,Hannah,1982-06-23T05:41:26,E
+6,Eleanor,1991-08-10T23:12:21,F
+7,Lily,1971-03-29T14:58:02,G
+8,Jonathan,1988-02-26T02:55:24,H
+9,Adrian,1994-02-09T13:14:23,I
+10,Nora,1976-03-01T16:51:39,J
+""".lstrip()
+
+schema_base_yml = """
+version: 2
+sources:
+  - name: raw
+    schema: "{{ target.schema }}"
+    tables:
+      - name: seed
+        identifier: "{{ var('seed_name', 'base') }}"
+"""
+
+schema_test_seed_yml = """
+version: 2
+models:
+  - name: base
+    columns:
+     - name: id
+       tests:
+         - not_null
+"""
+
+schema_test_view_yml = """
+version: 2
+models:
+  - name: view_model
+    columns:
+     - name: id
+       tests:
+         - not_null
+"""
+
+schema_test_table_yml = """
+version: 2
+models:
+  - name: table_model
+    columns:
+     - name: id
+       tests:
+         - not_null
+"""
+
+test_passing_sql = """
+select * from (
+    select 1 as id
+) as my_subquery
+where id = 2
+"""
+
+test_failing_sql = """
+select * from (
+    select 1 as id
+) as my_subquery
+where id = 1
+"""
+
+test_ephemeral_passing_sql = """
+with my_other_cool_cte as (
+    select id, name from {{ ref('ephemeral') }}
+    where id > 1000
+)
+select name, id from my_other_cool_cte
+"""
+
+test_ephemeral_failing_sql = """
+with my_other_cool_cte as (
+    select id, name from {{ ref('ephemeral') }}
+    where id < 1000
+)
+select name, id from my_other_cool_cte
+"""
+
+model_incremental = """
+select * from {{ source('raw', 'seed') }}
+{% if is_incremental() %}
+where id > (select max(id) from {{ this }})
+{% endif %}
+""".strip()
+
+cc_all_snapshot_sql = """
+{% snapshot cc_all_snapshot %}
+    {{ config(
+        check_cols='all', unique_key='id', strategy='check',
+        target_database=database, target_schema=schema
+    ) }}
+    select * from {{ ref(var('seed_name', 'base')) }}
+{% endsnapshot %}
+""".strip()
+
+cc_name_snapshot_sql = """
+{% snapshot cc_name_snapshot %}
+    {{ config(
+        check_cols=['name'], unique_key='id', strategy='check',
+        target_database=database, target_schema=schema
+    ) }}
+    select * from {{ ref(var('seed_name', 'base')) }}
+{% endsnapshot %}
+""".strip()
+
+cc_date_snapshot_sql = """
+{% snapshot cc_date_snapshot %}
+    {{ config(
+        check_cols=['some_date'], unique_key='id', strategy='check',
+        target_database=database, target_schema=schema
+    ) }}
+    select * from {{ ref(var('seed_name', 'base')) }}
+{% endsnapshot %}
+""".strip()
+
+ts_snapshot_sql = """
+{% snapshot ts_snapshot %}
+    {{ config(
+        strategy='timestamp',
+        unique_key='id',
+        updated_at='some_date',
+        target_database=database,
+        target_schema=schema,
+    )}}
+    select * from {{ ref(var('seed_name', 'base')) }}
+{% endsnapshot %}
+""".strip()
+
+model_ephemeral_with_cte = """
+with my_cool_cte as (
+  select name, id from {{ ref('base') }}
+)
+select id, name from my_cool_cte where id is not null
+"""
+
+config_materialized_table = """
+  {{ config(materialized="table") }}
+"""
+
+config_materialized_view = """
+  {{ config(materialized="view") }}
+"""
+
+config_materialized_ephemeral = """
+  {{ config(materialized="ephemeral") }}
+"""
+
+config_materialized_incremental = """
+  {{ config(materialized="incremental") }}
+"""
+
+config_materialized_var = """
+  {{ config(materialized=var("materialized_var", "table"))}}
+"""
+
+model_base = """
+  select * from {{ source('raw', 'seed') }}
+"""
+
+model_ephemeral = """
+  select * from {{ ref('ephemeral') }}
+"""
+
+base_materialized_var_sql = config_materialized_var + model_base
+base_table_sql = config_materialized_table + model_base
+base_view_sql = config_materialized_view + model_base
+base_ephemeral_sql = config_materialized_ephemeral + model_base
+ephemeral_with_cte_sql = config_materialized_ephemeral + model_ephemeral_with_cte
+ephemeral_view_sql = config_materialized_view + model_ephemeral
+ephemeral_table_sql = config_materialized_table + model_ephemeral
+incremental_sql = config_materialized_incremental + model_incremental
diff --git a/tests/functional/adapter/test_base.py b/tests/functional/adapter/test_base.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_base.py
@@ -0,0 +1,99 @@
+import pytest
+from dbt.tests.util import run_dbt
+from dbt.tests.adapter import check_result_nodes_by_name
+from dbt.tests.adapter import relation_from_name, check_relation_types, check_relations_equal
+from tests.functional.adapter.files import (
+    seeds_base_csv,
+    base_view_sql,
+    base_table_sql,
+    base_materialized_var_sql,
+    schema_base_yml,
+)
+
+
+@pytest.fixture
+def models():
+    return {
+        "view_model.sql": base_view_sql,
+        "table_model.sql": base_table_sql,
+        "swappable.sql": base_materialized_var_sql,
+        "schema.yml": schema_base_yml,
+    }
+
+
+@pytest.fixture
+def seeds():
+    return {
+        "base.csv": seeds_base_csv,
+    }
+
+
+@pytest.fixture
+def project_config_update():
+    return {
+        "name": "base",
+    }
+
+
+def test_base(project):
+
+    # seed command
+    results = run_dbt(["seed"])
+    # seed result length
+    assert len(results) == 1
+
+    # run command
+    results = run_dbt()
+    # run result length
+    assert len(results) == 3
+
+    # names exist in result nodes
+    check_result_nodes_by_name(results, ["view_model", "table_model", "swappable"])
+
+    # check relation types
+    expected = {
+        "base": "table",
+        "view_model": "view",
+        "table_model": "table",
+        "swappable": "table",
+    }
+    check_relation_types(project.adapter, expected)
+
+    # base table rowcount
+    relation = relation_from_name(project.adapter, "base")
+    result = project.run_sql(f"select count(*) as num_rows from {relation}", fetch="one")
+    assert result[0] == 10
+
+    # relations_equal
+    check_relations_equal(project.adapter, ["base", "view_model", "table_model", "swappable"])
+
+    # check relations in catalog
+    catalog = run_dbt(["docs", "generate"])
+    assert len(catalog.nodes) == 4
+    assert len(catalog.sources) == 1
+
+    # run_dbt changing materialized_var to view
+    results = run_dbt(["run", "-m", "swappable", "--vars", "materialized_var: view"])
+    assert len(results) == 1
+
+    # check relation types, swappable is view
+    expected = {
+        "base": "table",
+        "view_model": "view",
+        "table_model": "table",
+        "swappable": "view",
+    }
+    check_relation_types(project.adapter, expected)
+
+    # run_dbt changing materialized_var to incremental
+    results = run_dbt(["run", "-m", "swappable", "--vars", "materialized_var: incremental"])
+    assert len(results) == 1
+
+    # check relation types, swappable is table
+    expected = {
+        "base": "table",
+        "view_model": "view",
+        "table_model": "table",
+        "swappable": "table",
+    }
+    check_relation_types(project.adapter, expected)
diff --git a/tests/functional/adapter/test_data_test_ephemerals.py b/tests/functional/adapter/test_data_test_ephemerals.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_data_test_ephemerals.py
@@ -0,0 +1,67 @@
+import pytest
+
+from dbt.tests.util import run_dbt
+from dbt.tests.adapter import check_result_nodes_by_name
+from tests.functional.adapter.files import (
+    seeds_base_csv,
+    ephemeral_with_cte_sql,
+    test_ephemeral_passing_sql,
+    test_ephemeral_failing_sql,
+    schema_base_yml,
+)
+
+
+@pytest.fixture
+def seeds():
+    return {
+        "base.csv": seeds_base_csv,
+    }
+
+
+@pytest.fixture
+def models():
+    return {
+        "ephemeral.sql": ephemeral_with_cte_sql,
+        "passing_model.sql": test_ephemeral_passing_sql,
+        "failing_model.sql": test_ephemeral_failing_sql,
+        "schema.yml": schema_base_yml,
+    }
+
+
+@pytest.fixture
+def tests():
+    return {
+        "passing.sql": test_ephemeral_passing_sql,
+        "failing.sql": test_ephemeral_failing_sql,
+    }
+
+
+@pytest.fixture
+def project_config_update():
+    return {
+        "name": "data_test_ephemeral_models",
+    }
+
+
+def test_data_test_ephemerals(project):
+    # check results from seed command
+    results = run_dbt(["seed"])
+    assert len(results) == 1
+    check_result_nodes_by_name(results, ["base"])
+
+    # Check results from test command
+    results = run_dbt(["test"])
+    assert len(results) == 2
+    check_result_nodes_by_name(results, ["passing", "failing"])
+
+    # Check result status
+    for result in results:
+        if result.node.name == "passing":
+            assert result.status == "pass"
+        elif result.node.name == "failing":
+            assert result.status == "fail"
+
+    # check results from run command
+    results = run_dbt()
+    assert len(results) == 2
+    check_result_nodes_by_name(results, ["failing_model", "passing_model"])
diff --git a/tests/functional/adapter/test_data_tests.py b/tests/functional/adapter/test_data_tests.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_data_tests.py
@@ -0,0 +1,36 @@
+import pytest
+from tests.functional.adapter.files import (
+    test_passing_sql,
+    test_failing_sql,
+)
+from dbt.tests.adapter import check_result_nodes_by_name
+from dbt.tests.util import run_dbt
+
+
+@pytest.fixture
+def tests():
+    return {
+        "passing.sql": test_passing_sql,
+        "failing.sql": test_failing_sql,
+    }
+
+
+@pytest.fixture
+def project_config_update():
+    return {"name": "data_tests"}
+
+
+def test_data_tests(project):
+    # test command
+    results = run_dbt(["test"])
+    assert len(results) == 2
+
+    # We have the right result nodes
+    check_result_nodes_by_name(results, ["passing", "failing"])
+
+    # Check result status
+    for result in results:
+        if result.node.name == "passing":
+            assert result.status == "pass"
+        elif result.node.name == "failing":
+            assert result.status == "fail"
diff --git a/tests/functional/adapter/test_empty.py b/tests/functional/adapter/test_empty.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_empty.py
@@ -0,0 +1,24 @@
+from dbt.tests.util import run_dbt
+import os
+
+
+def test_empty(project):
+    # check seed
+    results = run_dbt(["seed"])
+    assert len(results) == 0
+    run_results_path = os.path.join(project.project_root, "target", "run_results.json")
+    assert os.path.exists(run_results_path)
+
+    # check run
+    results = run_dbt(["run"])
+    assert len(results) == 0
+
+    catalog_path = os.path.join(project.project_root, "target", "catalog.json")
+    assert not os.path.exists(catalog_path)
+
+    # check catalog
+    catalog = run_dbt(["docs", "generate"])
+    assert os.path.exists(run_results_path)
+    assert os.path.exists(catalog_path)
+    assert len(catalog.nodes) == 0
+    assert len(catalog.sources) == 0
diff --git a/tests/functional/adapter/test_ephemeral.py b/tests/functional/adapter/test_ephemeral.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_ephemeral.py
@@ -0,0 +1,63 @@
+import pytest
+import os
+from dbt.tests.util import run_dbt, get_manifest
+from dbt.tests.adapter import check_relations_equal, check_result_nodes_by_name, relation_from_name
+from tests.functional.adapter.files import (
+    seeds_base_csv,
+    base_ephemeral_sql,
+    ephemeral_view_sql,
+    ephemeral_table_sql,
+    schema_base_yml,
+)
+
+
+@pytest.fixture
+def project_config_update():
+    return {"name": "ephemeral"}
+
+
+@pytest.fixture
+def seeds():
+    return {"base.csv": seeds_base_csv}
+
+
+@pytest.fixture
+def models():
+    return {
+        "ephemeral.sql": base_ephemeral_sql,
+        "view_model.sql": ephemeral_view_sql,
+        "table_model.sql": ephemeral_table_sql,
+        "schema.yml": schema_base_yml,
+    }
+
+
+def test_ephemeral(project):
+    # seed command
+    results = run_dbt(["seed"])
+    assert len(results) == 1
+    check_result_nodes_by_name(results, ["base"])
+
+    # run command
+    results = run_dbt(["run"])
+    assert len(results) == 2
+    check_result_nodes_by_name(results, ["view_model", "table_model"])
+
+    # base table rowcount
+    relation = relation_from_name(project.adapter, "base")
+    result = project.run_sql(f"select count(*) as num_rows from {relation}", fetch="one")
+    assert result[0] == 10
+
+    # relations equal
+    check_relations_equal(project.adapter, ["base", "view_model", "table_model"])
+
+    # catalog node count
+    catalog = run_dbt(["docs", "generate"])
+    catalog_path = os.path.join(project.project_root, "target", "catalog.json")
+    assert os.path.exists(catalog_path)
+    assert len(catalog.nodes) == 3
+    assert len(catalog.sources) == 1
+
+    # manifest (not in original)
+    manifest = get_manifest(project.project_root)
+    assert len(manifest.nodes) == 4
+    assert len(manifest.sources) == 1
diff --git a/tests/functional/adapter/test_incremental.py b/tests/functional/adapter/test_incremental.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_incremental.py
@@ -0,0 +1,61 @@
+import pytest
+from dbt.tests.util import run_dbt
+from tests.functional.adapter.files import (
+    seeds_base_csv,
+    seeds_added_csv,
+    schema_base_yml,
+    incremental_sql,
+)
+from dbt.tests.adapter import check_relations_equal, relation_from_name
+
+
+@pytest.fixture
+def project_config_update():
+    return {"name": "incremental"}
+
+
+@pytest.fixture
+def models():
+    return {"incremental.sql": incremental_sql, "schema.yml": schema_base_yml}
+
+
+@pytest.fixture
+def seeds():
+    return {"base.csv": seeds_base_csv, "added.csv": seeds_added_csv}
+
+
+def test_incremental(project):
+    # seed command
+    results = run_dbt(["seed"])
+    assert len(results) == 2
+
+    # base table rowcount
+    relation = relation_from_name(project.adapter, "base")
+    result = project.run_sql(f"select count(*) as num_rows from {relation}", fetch="one")
+    assert result[0] == 10
+
+    # added table rowcount
+    relation = relation_from_name(project.adapter, "added")
+    result = project.run_sql(f"select count(*) as num_rows from {relation}", fetch="one")
+    assert result[0] == 20
+
+    # run command
+    # the "seed_name" var changes the seed identifier in the schema file
+    results = run_dbt(["run", "--vars", "seed_name: base"])
+    assert len(results) == 1
+
+    # check relations equal
+    check_relations_equal(project.adapter, ["base", "incremental"])
+
+    # change seed_name var
+    # the "seed_name" var changes the seed identifier in the schema file
+    results = run_dbt(["run", "--vars", "seed_name: added"])
+    assert len(results) == 1
+
+    # check relations equal
+    check_relations_equal(project.adapter, ["added", "incremental"])
+
+    # get catalog from docs generate
+    catalog = run_dbt(["docs", "generate"])
+    assert len(catalog.nodes) == 3
+    assert len(catalog.sources) == 1
diff --git a/tests/functional/adapter/test_schema_tests.py b/tests/functional/adapter/test_schema_tests.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_schema_tests.py
@@ -0,0 +1,52 @@
+import pytest
+from dbt.tests.util import run_dbt
+from tests.functional.adapter.files import (
+    seeds_base_csv,
+    schema_test_seed_yml,
+    base_view_sql,
+    base_table_sql,
+    schema_base_yml,
+    schema_test_view_yml,
+    schema_test_table_yml,
+)
+
+
+@pytest.fixture
+def project_config_update():
+    return {"name": "schema_test"}
+
+
+@pytest.fixture
+def seeds():
+    return {
+        "base.csv": seeds_base_csv,
+        "schema.yml": schema_test_seed_yml,
+    }
+
+
+@pytest.fixture
+def models():
+    return {
+        "view_model.sql": base_view_sql,
+        "table_model.sql": base_table_sql,
+        "schema.yml": schema_base_yml,
+        "schema_view.yml": schema_test_view_yml,
+        "schema_table.yml": schema_test_table_yml,
+    }
+
+
+def test_schema_tests(project):
+    # seed command
+    results = run_dbt(["seed"])
+
+    # test command selecting base model
+    results = run_dbt(["test", "-m", "base"])
+    assert len(results) == 1
+
+    # run command
+    results = run_dbt(["run"])
+    assert len(results) == 2
+
+    # test command, all tests
+    results = run_dbt(["test"])
+    assert len(results) == 3
diff --git a/tests/functional/adapter/test_snapshot_check_cols.py b/tests/functional/adapter/test_snapshot_check_cols.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_snapshot_check_cols.py
@@ -0,0 +1,111 @@
+import pytest
+from dbt.tests.util import run_dbt
+from tests.functional.adapter.files import (
+    seeds_base_csv,
+    seeds_added_csv,
+    cc_all_snapshot_sql,
+    cc_date_snapshot_sql,
+    cc_name_snapshot_sql,
+)
+from dbt.tests.adapter import update_rows, relation_from_name
+
+
+@pytest.fixture
+def project_config_update():
+    return {"name": "snapshot_strategy_check_cols"}
+
+
+@pytest.fixture
+def seeds():
+    return {
+        "base.csv": seeds_base_csv,
+        "added.csv": seeds_added_csv,
+    }
+
+
+@pytest.fixture
+def snapshots():
+    return {
+        "cc_all_snapshot.sql": cc_all_snapshot_sql,
+        "cc_date_snapshot.sql": cc_date_snapshot_sql,
+        "cc_name_snapshot.sql": cc_name_snapshot_sql,
+    }
+
+
+def check_relation_rows(project, snapshot_name, count):
+    relation = relation_from_name(project.adapter, snapshot_name)
+    result = project.run_sql(f"select count(*) as num_rows from {relation}", fetch="one")
+    assert result[0] == count
+
+
+def test_snapshot_check_cols(project):
+    # seed command
+    results = run_dbt(["seed"])
+    assert len(results) == 2
+
+    # snapshot command
+    results = run_dbt(["snapshot"])
+    for result in results:
+        assert result.status == "success"
+
+    # check rowcounts for all snapshots
+    check_relation_rows(project, "cc_all_snapshot", 10)
+    check_relation_rows(project, "cc_name_snapshot", 10)
+    check_relation_rows(project, "cc_date_snapshot", 10)
+
+    relation = relation_from_name(project.adapter, "cc_all_snapshot")
+    result = project.run_sql(f"select * from {relation}", fetch="all")
+
+    # point at the "added" seed so the snapshot sees 10 new rows
+    results = run_dbt(["--no-partial-parse", "snapshot", "--vars", "seed_name: added"])
+    for result in results:
+        assert result.status == "success"
+
+    # check rowcounts for all snapshots
+    check_relation_rows(project, "cc_all_snapshot", 20)
+    check_relation_rows(project, "cc_name_snapshot", 20)
+    check_relation_rows(project, "cc_date_snapshot", 20)
+
+    # update some timestamps in the "added" seed so the snapshot sees 10 more new rows
+    update_rows_config = {
+        "name": "added",
+        "dst_col": "some_date",
+        "clause": {"src_col": "some_date", "type": "add_timestamp"},
+        "where": "id > 10 and id < 21",
+    }
+    update_rows(project.adapter, update_rows_config)
+
+    # re-run snapshots, using "added'
+    results = run_dbt(["snapshot", "--vars", "seed_name: added"])
+    for result in results:
+        assert result.status == "success"
+
+    # check rowcounts for all snapshots
+    check_relation_rows(project, "cc_all_snapshot", 30)
+    check_relation_rows(project, "cc_date_snapshot", 30)
+    # unchanged: only the timestamp changed
+    check_relation_rows(project, "cc_name_snapshot", 20)
+
+    # Update the name column
+    update_rows_config = {
+        "name": "added",
+        "dst_col": "name",
+        "clause": {
+            "src_col": "name",
+            "type": "add_string",
+            "value": "_updated",
+        },
+        "where": "id < 11",
+    }
+    update_rows(project.adapter, update_rows_config)
+
+    # re-run snapshots, using "added'
+    results = run_dbt(["snapshot", "--vars", "seed_name: added"])
+    for result in results:
+        assert result.status == "success"
+
+    # check rowcounts for all snapshots
+    check_relation_rows(project, "cc_all_snapshot", 40)
+    check_relation_rows(project, "cc_name_snapshot", 30)
+    # does not see name updates
+    check_relation_rows(project, "cc_date_snapshot", 30)
diff --git a/tests/functional/adapter/test_snapshot_timestamp.py b/tests/functional/adapter/test_snapshot_timestamp.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/adapter/test_snapshot_timestamp.py
@@ -0,0 +1,89 @@
+import pytest
+from dbt.tests.util import run_dbt
+from tests.functional.adapter.files import (
+    seeds_base_csv,
+    seeds_newcolumns_csv,
+    seeds_added_csv,
+    ts_snapshot_sql,
+)
+from dbt.tests.adapter import relation_from_name, update_rows
+
+
+@pytest.fixture
+def seeds():
+    return {
+        "base.csv": seeds_base_csv,
+        "newcolumns.csv": seeds_newcolumns_csv,
+        "added.csv": seeds_added_csv,
+    }
+
+
+@pytest.fixture
+def snapshots():
+    return {
+        "ts_snapshot.sql": ts_snapshot_sql,
+    }
+
+
+@pytest.fixture
+def project_config_update():
+    return {"name": "snapshot_strategy_timestamp"}
+
+
+def check_relation_rows(project, snapshot_name, count):
+    relation = relation_from_name(project.adapter, snapshot_name)
+    result = project.run_sql(f"select count(*) as num_rows from {relation}", fetch="one")
+    assert result[0] == count
+
+
+def test_snapshot_timestamp(project):
+    # seed command
+    results = run_dbt(["seed"])
+    assert len(results) == 3
+
+    # snapshot command
+    results = run_dbt(["snapshot"])
+    assert len(results) == 1
+
+    # snapshot has 10 rows
+    check_relation_rows(project, "ts_snapshot", 10)
+
+    # point at the "added" seed so the snapshot sees 10 new rows
+    results = run_dbt(["snapshot", "--vars", "seed_name: added"])
+
+    # snapshot now has 20 rows
+    check_relation_rows(project, "ts_snapshot", 20)
+
+    # update some timestamps in the "added" seed so the snapshot sees 10 more new rows
+    update_rows_config = {
+        "name": "added",
+        "dst_col": "some_date",
+        "clause": {
+            "src_col": "some_date",
+            "type": "add_timestamp",
+        },
+        "where": "id > 10 and id < 21",
+    }
+    update_rows(project.adapter, update_rows_config)
+
+    results = run_dbt(["snapshot", "--vars", "seed_name: added"])
+
+    # snapshot now has 30 rows
+    check_relation_rows(project, "ts_snapshot", 30)
+
+    update_rows_config = {
+        "name": "added",
+        "dst_col": "name",
+        "clause": {
+            "src_col": "name",
+            "type": "add_string",
+            "value": "_updated",
+        },
+        "where": "id < 11",
+    }
+    update_rows(project.adapter, update_rows_config)
+
+    results = run_dbt(["snapshot", "--vars", "seed_name: added"])
+
+    # snapshot still has 30 rows because timestamp not updated
+    check_relation_rows(project, "ts_snapshot", 30)
diff --git a/tests/functional/basic/test_jaffle_shop.py b/tests/functional/basic/test_jaffle_shop.py
--- a/tests/functional/basic/test_jaffle_shop.py
+++ b/tests/functional/basic/test_jaffle_shop.py
@@ -1,15 +1,16 @@
 from dbt.tests.util import run_dbt, get_manifest
 
 
-from tests.fixtures.jaffle_shop import models, seeds, project_config_update  # noqa: F401
+from tests.fixtures.jaffle_shop import JaffleShopProject
 
 
-def test_basic(project):
-    # Create the data from seeds
-    results = run_dbt(["seed"])
+class TestBasic(JaffleShopProject):
+    def test_basic(self, project):
+        # Create the data from seeds
+        results = run_dbt(["seed"])
 
-    # Tests that the jaffle_shop project runs
-    results = run_dbt(["run"])
-    assert len(results) == 5
-    manifest = get_manifest(project.project_root)
-    assert "model.jaffle_shop.orders" in manifest.nodes
+        # Tests that the jaffle_shop project runs
+        results = run_dbt(["run"])
+        assert len(results) == 5
+        manifest = get_manifest(project.project_root)
+        assert "model.jaffle_shop.orders" in manifest.nodes