Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 09.05 · 22 мин
Продвинутый
AdapterDebugValidationFirst steps

dbt debug flow: что должно работать минимально

dbt debug — это первый тест, который должен пройти любой adapter. Если debug passes — у вас есть рабочее соединение с warehouse, и можно начинать тестировать. Если падает — что-то фундаментально сломано.

В этом уроке разбираем, что именно dbt debug делает, какие методы вашего adapter’а он использует, и как minimal реализация выглядит для passing debug.


dbt debug, dbt compile, dbt show: три команды дебага (dbt I)

Что делает dbt debug

dbt debug выполняет несколько проверок, чтобы убедиться что dbt может работать с вашим warehouse:

dbt debug checks
1. profiles.yml syntax checkdbt debug читает profiles.yml, валидирует YAML structure. Catches syntax errors, missing required fields.
2. Credentials instantiationdbt создаёт Credentials instance из YAML. Применяет ALIASES, валидирует через __post_init__.
3. AdapterPlugin lookupdbt находит AdapterPlugin для type='oceanbase'. Если plugin не зарегистрирован — debug fails.
4. Connection.open()ConnectionManager.open() — actually connect к warehouse. TLS, auth, session setup.
5. SELECT 1 test queryRun trivial SELECT (e.g., SELECT 1). Проверяет cursor.execute() работает.
6. List schemasadapter.list_schemas() — get list of schemas. Проверяет что macro работает.
7. Check target schema existsadapter.check_schema_exists() — проверяет существование target schema.
8. Summary reportAll checks pass -> 'All checks passed!' Otherwise — error messages с context.

Если все 8 шагов pass — adapter готов для basic use. Иначе debug говорит, какой step failed.


Output successful debug

$ dbt debug --profiles-dir . --profile dbt-oceanbase

Running with dbt=1.8.5
dbt version: 1.8.5
python version: 3.11.6
python path: /opt/venv/bin/python
os info: Linux-5.15.0-x86_64

Profile setup:
  profiles.yml file [OK found and valid]
  dbt_project.yml file [OK found and valid]

Profile: dbt-oceanbase

Configuration:
  Required dependencies:
   - git [OK found]

Connection:
  host: localhost
  port: 2881
  user: root
  database: test
  schema: public
  Connection test: [OK connection ok]
  Required adapter capabilities: [OK all supported]

All checks passed!

Если что-то падает — debug пишет error на том месте. Easy для localization.


Что minimal нужно для passing debug

Чтобы dbt debug passes, ваш adapter должен иметь:

1. Properly зарегистрированный AdapterPlugin

# dbt/adapters/oceanbase/__init__.py
from dbt.adapters.oceanbase.connections import OceanBaseConnectionManager
from dbt.adapters.oceanbase.credentials import OceanBaseCredentials
from dbt.adapters.oceanbase.impl import OceanBaseAdapter
from dbt.adapters.base import AdapterPlugin
from dbt.include import oceanbase

Plugin = AdapterPlugin(
    adapter=OceanBaseAdapter,
    credentials=OceanBaseCredentials,
    include_path=oceanbase.PACKAGE_PATH,
)

И в dbt/include/oceanbase/__init__.py:

from pathlib import Path
PACKAGE_PATH = Path(__file__).parent

Без этого dbt не знает что adapter существует.

2. Credentials с required fields

Required fields в Credentials должны соответствовать profiles.yml:

@dataclass
class OceanBaseCredentials(Credentials):
    host: str       # ← required
    user: str       # ← required
    password: str   # ← required
    database: str   # ← inherited from Credentials base
    schema: str     # ← inherited
    
    port: int = 2881  # ← optional с default
    
    @property
    def type(self) -> str:
        return 'oceanbase'

Если profile_dir profiles.yml не имеет required field — debug fails immediately.

3. ConnectionManager с working open()

Минимум для open():

@classmethod
def open(cls, connection):
    if connection.state == ConnectionState.OPEN:
        return connection
    
    credentials = connection.credentials
    handle = pymysql.connect(
        host=credentials.host,
        port=credentials.port,
        user=credentials.user,
        password=credentials.password,
        database=credentials.database,
    )
    
    connection.handle = handle
    connection.state = ConnectionState.OPEN
    return connection

Должно вернуть Connection с handle != None.

4. Working execute() через cursor

Для SELECT 1 test query — нужен work через SQLAdapter.execute():

class OceanBaseAdapter(SQLAdapter):
    ConnectionManager = OceanBaseConnectionManager
    
    # execute() inherited from SQLAdapter
    # Uses self.add_query() -> connection.handle.cursor().execute(sql)

Если ваш connection.handle имеет standard cursor API — это inherited code работает.

5. list_schemas macro

Для check 6 (list schemas) — нужен:

-- dbt/include/oceanbase/macros/adapters.sql
{% macro oceanbase__list_schemas(database) %}
  {% call statement('list_schemas', fetch_result=True, auto_begin=False) %}
    SHOW DATABASES
  {% endcall %}
  {{ return(load_result('list_schemas').table) }}
{% endmacro %}

Или default version из SQLAdapter работает для ANSI SQL warehouses (information_schema.schemata).

6. check_schema_exists macro

{% macro oceanbase__check_schema_exists(information_schema, schema) %}
  {% call statement('check_schema_exists', fetch_result=True, auto_begin=False) %}
    SELECT COUNT(*) FROM information_schema.schemata
    WHERE schema_name = '{{ schema }}'
  {% endcall %}
  {{ return(load_result('check_schema_exists').table) }}
{% endmacro %}

Минимум кода для passing debug

Полный minimal adapter для passing debug (без работы dbt run):

# dbt/adapters/myadapter/credentials.py
from dataclasses import dataclass
from dbt.adapters.contracts.connection import Credentials

@dataclass
class MyAdapterCredentials(Credentials):
    host: str
    user: str = ''
    password: str = ''
    database: str = ''
    schema: str = ''
    port: int = 5432
    
    @property
    def type(self):
        return 'myadapter'
    
    @property
    def unique_field(self):
        return self.host
    
    def _connection_keys(self):
        return ('host', 'port', 'user', 'database', 'schema')
# dbt/adapters/myadapter/connections.py
from contextlib import contextmanager
import sqlite3

from dbt.adapters.base import BaseConnectionManager
from dbt.adapters.contracts.connection import (
    AdapterResponse, Connection, ConnectionState,
)


class MyAdapterConnectionManager(BaseConnectionManager):
    TYPE = 'myadapter'

    @classmethod
    def open(cls, connection):
        if connection.state == ConnectionState.OPEN:
            return connection
        
        # Use SQLite for demo
        handle = sqlite3.connect(':memory:')
        connection.handle = handle
        connection.state = ConnectionState.OPEN
        return connection

    @classmethod
    def get_response(cls, cursor):
        return AdapterResponse(_message='OK', rows_affected=cursor.rowcount)

    @contextmanager
    def exception_handler(self, sql):
        try:
            yield
        except sqlite3.Error as e:
            self.release()
            raise RuntimeError(str(e))

    def cancel(self, connection):
        connection.handle.close()
# dbt/adapters/myadapter/impl.py
from dbt.adapters.sql import SQLAdapter
from dbt.adapters.myadapter.connections import MyAdapterConnectionManager


class MyAdapterAdapter(SQLAdapter):
    ConnectionManager = MyAdapterConnectionManager
    # Relation = ... (use default for now)
    # Column = ... (use default)
    
    @classmethod
    def date_function(cls):
        return 'DATE(\\'now\\')'  # SQLite syntax
    
    @classmethod
    def convert_text_type(cls, agate_table, col_idx):
        return 'TEXT'
    
    @classmethod
    def convert_number_type(cls, agate_table, col_idx):
        return 'NUMERIC'
    
    @classmethod
    def convert_boolean_type(cls, agate_table, col_idx):
        return 'BOOLEAN'
    
    @classmethod
    def convert_datetime_type(cls, agate_table, col_idx):
        return 'DATETIME'
    
    @classmethod
    def convert_date_type(cls, agate_table, col_idx):
        return 'DATE'
    
    @classmethod
    def convert_time_type(cls, agate_table, col_idx):
        return 'TIME'
# dbt/adapters/myadapter/__init__.py
from dbt.adapters.myadapter.connections import MyAdapterConnectionManager
from dbt.adapters.myadapter.credentials import MyAdapterCredentials
from dbt.adapters.myadapter.impl import MyAdapterAdapter
from dbt.adapters.base import AdapterPlugin
from dbt.include import myadapter

Plugin = AdapterPlugin(
    adapter=MyAdapterAdapter,
    credentials=MyAdapterCredentials,
    include_path=myadapter.PACKAGE_PATH,
)
# dbt/include/myadapter/__init__.py
from pathlib import Path
PACKAGE_PATH = Path(__file__).parent
-- dbt/include/myadapter/macros/adapters.sql
{% macro myadapter__list_schemas(database) %}
  {% call statement('list_schemas', fetch_result=True, auto_begin=False) %}
    SELECT name AS schema_name FROM sqlite_master WHERE type='table'
  {% endcall %}
  {{ return(load_result('list_schemas').table) }}
{% endmacro %}

{% macro myadapter__check_schema_exists(information_schema, schema) %}
  {% call statement('check_schema_exists', fetch_result=True, auto_begin=False) %}
    SELECT 1 AS schema_exists
  {% endcall %}
  {{ return(load_result('check_schema_exists').table) }}
{% endmacro %}

{% macro myadapter__create_schema(relation) %}
  {# SQLite не имеет CREATE SCHEMA, noop #}
{% endmacro %}

{% macro myadapter__drop_schema(relation) %}
  {# SQLite не имеет DROP SCHEMA, noop #}
{% endmacro %}
# dbt/include/myadapter/dbt_project.yml
config-version: 2
name: myadapter
version: '1.0'

profile: 'default'

target-path: 'target'
clean-targets: ['target', 'dbt_packages']

models:
  myadapter:
    +materialized: view
# setup.py
from setuptools import setup, find_namespace_packages

setup(
    name='dbt-myadapter',
    version='1.0.0',
    packages=find_namespace_packages(include=['dbt.*']),
    package_data={
        'dbt': [
            'include/myadapter/dbt_project.yml',
            'include/myadapter/macros/*.sql',
        ],
    },
    install_requires=[
        'dbt-core>=1.8',
        'dbt-adapters>=1.7',
    ],
)

Install:

pip install -e .

Profile:

# ~/.dbt/profiles.yml
test-myadapter:
  target: dev
  outputs:
    dev:
      type: myadapter
      host: localhost
      user: test
      database: test
      schema: main

Run:

dbt debug --profile test-myadapter
# Should print: 'All checks passed!'

Common debug failures и fixes

Failure 1: Adapter not found

Encountered an error:
  Runtime Error
    Could not find adapter type 'myadapter'!

Cause: AdapterPlugin не зарегистрирован.

Debug:

python -c "from dbt.adapters.factory import FACTORY; print(FACTORY.plugins.keys())"

Если ‘myadapter’ не в keys — Plugin не registered.

Fix: ensure pip install -e . succeeded. Check dbt/adapters/myadapter/__init__.py имеет Plugin = AdapterPlugin(...).

Failure 2: Missing required field

Encountered an error:
  Compilation Error
    Credentials in profile "test-myadapter", target "dev" invalid:
    'host' is a required property

Cause: profiles.yml не имеет required field defined в Credentials dataclass.

Fix: add host: к profiles.yml или make optional в dataclass.

Failure 3: Connection failed

Encountered an error:
  Database Error
    Could not connect to database:
    [Errno 111] Connection refused

Cause: warehouse не запущен на host/port.

Debug:

nc -zv localhost 5432   # Postgres
nc -zv localhost 2881   # OceanBase

If connection refused — warehouse не slušа на этом port.

Fix: start warehouse, verify port, check firewall.

Failure 4: Authentication failed

Encountered an error:
  Database Error
    Authentication failed for user 'wrong_user'

Cause: wrong credentials.

Fix: verify user/password/auth method.

Failure 5: list_schemas macro fails

Encountered an error:
  Database Error in query 'list_schemas'
    no such function: information_schema.schemata

Cause: default list_schemas использует ANSI SQL, не работает на SQLite/non-standard warehouses.

Fix: override через <adapter>__list_schemas macro.


debug exit codes

dbt debug; echo $?
  • 0: all checks passed
  • 2: some checks failed
  • 1: unknown error

Use в CI:

- name: Verify dbt can connect
  run: dbt debug

CI fails if debug doesn’t pass — early detection of config issues.


Попробуй сам

  1. Создайте minimal adapter scaffold (как выше).

  2. pip install -e . для регистрации.

  3. Create profiles.yml.

  4. Run dbt debug. Should pass.

  5. Намеренно сломайте:

    • Remove host from profiles.yml — должно fail на step 2
    • Wrong port в profiles.yml — должно fail на step 4 (connection)
    • Remove oceanbase__list_schemas macro — должно fail на step 6 (if no fallback)
  6. Add to CI:

    - run: dbt debug

    Verify CI fails when config broken.


Ключевые выводы

  1. dbt debug — 8 checks: YAML syntax -> Credentials -> Plugin lookup -> open() -> SELECT 1 -> list_schemas -> check_schema_exists -> summary.

  2. Minimum для passing debug: registered AdapterPlugin, working Credentials, ConnectionManager с open()/get_response()/cancel(), basic execute(), list_schemas macro.

  3. Inherit from SQLAdapter — большинство getting работает out-of-box (cursor execute, ANSI SQL macros).

  4. Override адаптер-specific только when default не работает (warehouse-specific SQL, non-standard types).

  5. debug — gate в CI. Pre-deploy validation. exit code 0 = OK, 2 = fail.

  6. Common failures: adapter not found (pip install), connection refused (warehouse not running), authentication failed (wrong creds), macros (override default).

Проверка знанийKnowledge check
Senior пишет adapter для proprietary warehouse 'KronosDB'. dbt debug passes на macos, но fails в Docker CI. Что искать?
ОтветAnswer
Несколько потенциальных причин — environment-specific.\n\n**Cause 1 — Different Python version**:\n\nDocker CI может использовать Python 3.9, dev — 3.11. Some library may behave differently.\n\nDebug:\n\n```bash\n# In Dockerfile\nRUN python --version\nRUN pip show dbt-kronosdb\n```\n\nVerify same versions.\n\n**Cause 2 — Missing system dependencies**:\n\nKronosDB client может require system libraries (e.g., `libssl`, `libldap`). dev machine имеет, Docker minimal image не имеет.\n\nDebug:\n\n```bash\nldd $(python -c "import kronosdb; print(kronosdb.__file__)")\n```\n\nIf 'not found' errors — missing system libs.\n\nFix: в Dockerfile install:\n\n```dockerfile\nRUN apt-get update && apt-get install -y \\\n libssl-dev libsasl2-dev libldap2-dev\n```\n\n**Cause 3 — DNS / Network issues в Docker**:\n\nDev machine имеет corporate DNS, Docker uses Google's 8.8.8.8. KronosDB internal hostname не resolves.\n\nDebug:\n\n```bash\ndocker run --rm myimage nslookup kronos.internal.corp\n```\n\nFix: --dns option:\n\n```bash\ndocker run --dns 10.0.0.1 ...\n```\n\nOr corporate VPN inside Docker.\n\n**Cause 4 — Missing env vars**:\n\nDev shell имеет credentials в env vars (.bashrc). Docker не. profiles.yml uses `env_var('KRONOSDB_PASSWORD')` — fails.\n\nDebug:\n\n```bash\ndocker run --rm myimage env | grep KRONOSDB\n# Should show vars set\n```\n\nFix: pass через docker run:\n\n```bash\ndocker run -e KRONOSDB_PASSWORD=$KRONOSDB_PASSWORD ...\n```\n\nOr в CI:\n\n```yaml\n- run: docker run -e KRONOSDB_PASSWORD=${{ secrets.KRONOSDB_PASSWORD }} myimage\n```\n\n**Cause 5 — Different timezone / locale**:\n\nKronosDB requires specific timezone. Dev machine UTC, Docker UTC-8. Connection setup может fail.\n\nDebug:\n\n```bash\ndocker run --rm myimage date\ndocker run --rm myimage locale\n```\n\nFix:\n\n```dockerfile\nENV TZ=UTC\nRUN apt-get install -y tzdata\n```\n\n**Cause 6 — Volume / file mount issues**:\n\nDocker не mounts `~/.dbt/` properly. profiles.yml не accessible inside container.\n\nDebug:\n\n```bash\ndocker run --rm myimage cat /root/.dbt/profiles.yml\n# Should show profile contents\n```\n\nIf empty / not found — volume mount issue.\n\nFix:\n\n```bash\ndocker run -v ~/.dbt:/root/.dbt myimage\n```\n\n**Cause 7 — pip install не finds package**:\n\nDev installed с `pip install -e .` (editable). Docker copy + install. Если `pyproject.toml` / `setup.py` имеют issues — install incomplete.\n\nDebug:\n\n```bash\ndocker run --rm myimage python -c "from dbt.adapters.kronosdb import Plugin; print(Plugin)"\n```\n\nIf ImportError — package не installed correctly в container.\n\nFix: verify Dockerfile:\n\n```dockerfile\nCOPY . /app\nWORKDIR /app\nRUN pip install -e .\n```\n\nNot:\n\n```dockerfile\nCOPY dbt_kronosdb /app # missing setup.py!\n```\n\n**Cause 8 — Different KronosDB client library version**:\n\nDev requires `kronosdb-python==1.5`. Docker installed `kronosdb-python==2.0` (latest). New API breaks compatibility.\n\nDebug:\n\n```bash\npip freeze | grep kronos\n# Compare dev vs Docker\n```\n\nFix: pin в requirements.txt:\n\n```\nkronosdb-python==1.5\n```\n\n**Cause 9 — SELinux / AppArmor**:\n\nNot all systems, but for hardened CI:\n\nDocker imposes restrictions on network calls. KronosDB connection blocked.\n\nDebug:\n\n```bash\ndocker run --security-opt seccomp=unconfined myimage dbt debug\n# Если works без restriction — SELinux/seccomp issue\n```\n\n**Cause 10 — VPN / proxy required**:\n\nDev machine on corporate VPN. Docker is not. KronosDB only accessible via VPN.\n\nFix: either:\n\n- Run Docker inside VPN\n- Use proxy: `HTTPS_PROXY=http://proxy.corp:8080`\n- Run CI on-premise (corporate runner) instead of cloud\n\n**Systematic debug approach**:\n\n```bash\n# 1. Verify package installed\ndocker run --rm myimage python -c "from dbt.adapters.kronosdb import Plugin"\n\n# 2. Verify env vars\ndocker run --rm -e KRONOSDB_PASSWORD=$PWD myimage env | grep KRONOSDB\n\n# 3. Verify network\ndocker run --rm myimage nc -zv kronos.host 5432\n\n# 4. Verify DNS\ndocker run --rm myimage nslookup kronos.host\n\n# 5. Run debug with verbose\ndocker run --rm myimage dbt --debug debug 2>&1 | tee /tmp/debug.log\n\n# 6. Compare logs dev vs Docker\n```\n\n**Production discipline**:\n\n1. **Reproducible Docker image** — pin Python, system deps, library versions.\n2. **Same env vars** in dev и CI.\n3. **Test connectivity** в Dockerfile itself (e.g., `RUN dbt debug || true` чтобы видеть logs).\n4. **Document required network access** в README.\n5. **CI runners в same network** as warehouse.\n\nЭто **DevOps + adapter knowledge** combo. Senior должен уметь debug both layers.
Проверка знанийKnowledge check
Какие methods adapter обязательно нужны для dbt debug, vs dbt run, vs dbt test?
ОтветAnswer
Разные dbt commands используют разные parts of adapter API. Знать это helps в incremental adapter development.\n\n**dbt debug — minimum viable**:\n\n1. `AdapterPlugin` registered\n2. `Credentials` с required fields\n3. `ConnectionManager.open()` — works\n4. `ConnectionManager.get_response()` — returns AdapterResponse\n5. `adapter.execute()` — runs SELECT 1\n6. `<adapter>__list_schemas` macro (or ANSI default)\n7. `<adapter>__check_schema_exists` macro\n\n**Что НЕ нужно** для debug:\n\n- list_relations_without_caching\n- get_columns_in_relation\n- materialization macros\n- DDL macros (create/drop)\n\n**Если debug passes** — adapter может connect. Это первая milestone.\n\n**dbt run — adds DDL/DML**:\n\nNeeds additionally:\n\n1. `create_schema` macro — create target schema if not exists\n2. `drop_schema` macro — для dbt clean\n3. `get_columns_in_relation` — для type checking, view-only materialization wouldn't need but generally yes\n4. `list_relations_without_caching` — for cache initialization\n5. `create_table_as` macro — для table materialization\n6. `create_view_as` macro — для view materialization\n7. `rename_relation` macro — для backup-rename swap\n8. `drop_relation` macro — для cleanup\n9. Materializations:\n - `materialization view, <adapter>` (or fall back to default)\n - `materialization table, <adapter>` (or default)\n - `materialization incremental, <adapter>` (or default)\n\n**dbt run failure cascade**:\n\n- Missing `create_schema` -> schema not created -> CREATE TABLE fails on missing schema\n- Missing `get_columns_in_relation` -> cache empty -> contract checks fail\n- Missing `drop_relation` -> cleanup fails, leftover backup tables\n\n**dbt test — adds query execution**:\n\nNeeds additionally:\n\n1. `adapter.execute(sql, fetch=True)` — must return Table\n2. `get_result_from_cursor` — translate cursor to agate.Table\n3. `current_timestamp` macro — для test_timestamp_format tests\n4. Macro `run_check_dataset` или similar — test execution\n\n**dbt seed — adds CSV -> SQL**:\n\nNeeds:\n\n1. `convert_text_type` / etc. — для type conversions\n2. `get_csv_sql` macro — INSERT INTO from values\n3. `load_csv_rows` macro — bulk load\n\n**dbt snapshot — adds SCD2 logic**:\n\nNeeds:\n\n1. `snapshot_string_as_time` macro — timestamp parsing\n2. `snapshot_check_strategy` / `snapshot_timestamp_strategy`\n3. `build_snapshot_table` macro — snapshot SQL generation\n\n**dbt source freshness — adds time arithmetic**:\n\nNeeds:\n\n1. `current_timestamp` macro\n2. `get_source_relation_max_updated_at` macro\n\n**dbt docs generate — adds catalog**:\n\nNeeds:\n\n1. `get_catalog` macro — query metadata for всех relations\n2. `get_columns_in_relation` для каждого relation\n\n**dbt-tests-adapter suite — full coverage**:\n\nЭто полная testing. Если все pass — adapter полно-functional.\n\n**Incremental development strategy**:\n\n**Week 1 — debug works**:\n\n- Credentials, ConnectionManager.open\n- Test `dbt debug` passes\n\n**Week 2 — basic run works**:\n\n- create_schema, list_relations, create_view_as\n- Test simple view materialization\n\n**Week 3 — table materialization**:\n\n- create_table_as, drop_relation, rename_relation\n- Test table materialization, replace existing\n\n**Week 4 — incremental**:\n\n- get_incremental_merge_sql, get_incremental_delete_insert_sql\n- Test incremental updates\n\n**Week 5 — tests, seeds**:\n\n- Type conversions, get_csv_sql\n- Test data tests, seeds\n\n**Week 6 — snapshots**:\n\n- Snapshot strategies\n- Test SCD2\n\n**Weeks 7-12 — full dbt-tests-adapter suite pass**.\n\nThis is **iterative approach**. Each milestone gives working dbt for that command set. Easier debugging чем 'all at once'.\n\n**For Trusted Adapter Program** — full suite pass + ongoing maintenance. Months 6-12 для production-ready.\n\nЭто **roadmap for adapter development**. Senior должен plan accordingly.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5