Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 09.01 · 25 мин
Продвинутый
AdapterBaseAdapterSQLAdapterPythonOOP

SQLAdapter vs BaseAdapter: что и когда наследовать

Если materializations — это Jinja-уровень работы с warehouse, то adapter — это Python-уровень. Adapter — это класс, который реализует «как именно dbt разговаривает с конкретным warehouse». Connection management, type mapping, execution, кэширование, грантс — всё это в adapter’е.

В этом модуле мы пишем свой adapter с нуля. Не для production (это путь к Trusted Adapter Program — занимает месяцы), но для понимания. После курса вы сможете читать source code dbt-snowflake, dbt-bigquery, dbt-duckdb как родной язык.

Class как PyTypeObject: всё, чем является объект Python Что такое реляция и реляционная модель

Начинаем с выбора базового класса.


Два базовых класса в dbt-adapters

В пакете dbt-adapters (отдельный от dbt-core с релиза 1.8) есть два главных базовых класса для adapter’ов:

BaseAdapter vs SQLAdapter
BaseAdapterBaseAdapter — абстрактный базовый класс. Минимальный API для любого источника данных. Подходит для не-SQL источников: NoSQL, REST API, lakehouse без SQL engine.
SQLAdapter (extends BaseAdapter)SQLAdapter наследует BaseAdapter и добавляет SQL-specific helpers. Подходит для любого SQL warehouse — это 95% случаев.

Простое правило: если ваш warehouse работает через SQL-statements и cursor.execute(sql) — используйте SQLAdapter. Если через DataFrame API, REST endpoints или proprietary protocol — BaseAdapter.


Архитектура BaseAdapter

# dbt-adapters/dbt/adapters/base/impl.py (упрощённо)
class BaseAdapter:
    """
    Abstract base class for all adapters.
    Defines minimum API surface dbt expects.
    """

    ConnectionManager: Type[BaseConnectionManager]
    Relation: Type[BaseRelation]
    Column: Type[Column]

    def __init__(self, config: AdapterRequiredConfig):
        self.config = config
        self.connections = self.ConnectionManager(config)
        self.cache = RelationsCache()

    # === REQUIRED ABSTRACT METHODS ===

    @abstractmethod
    def execute(self, sql: str, ...) -> Tuple[AdapterResponse, Table]:
        """Execute SQL and return (response, result)"""

    @abstractmethod
    def get_columns_in_relation(self, relation: BaseRelation) -> List[Column]:
        """Get column info for relation"""

    @abstractmethod
    def list_relations_without_caching(self, schema_relation: BaseRelation) -> List[BaseRelation]:
        """List all relations in schema"""

    @abstractmethod
    def create_schema(self, relation: BaseRelation) -> None:
        """Create schema in warehouse"""

    @abstractmethod
    def drop_schema(self, relation: BaseRelation) -> None:
        """Drop schema in warehouse"""

    # ... ещё ~20 abstract methods

    # === OPTIONAL OVERRIDES ===

    def get_relation(self, database: str, schema: str, identifier: str) -> Optional[BaseRelation]:
        """Get relation from cache or warehouse"""
        # Default implementation uses cache + list_relations_without_caching

Видно, что BaseAdapter определяет абстрактные методы (@abstractmethod), которые подклассы обязаны реализовать. И optional overrides — методы с default implementation, которые можно переопределить.


Архитектура SQLAdapter

# dbt-adapters/dbt/adapters/sql/impl.py (упрощённо)
class SQLAdapter(BaseAdapter):
    """
    Extends BaseAdapter with SQL-specific helpers.
    Most warehouse adapters inherit from this.
    """

    # === SQL-specific helpers ===

    def add_query(self, sql: str, ...) -> Tuple[Connection, Any]:
        """Execute single SQL query, return cursor result"""
        conn = self.connections.get_thread_connection()
        cursor = conn.handle.cursor()
        cursor.execute(sql)
        return conn, cursor

    def execute(self, sql: str, fetch: bool = False, ...) -> Tuple[AdapterResponse, Table]:
        """Default execute implementation using add_query"""
        _, cursor = self.add_query(sql)
        response = self.get_response(cursor)
        if fetch:
            return response, self.get_result_from_cursor(cursor)
        return response, agate.Table([])

    # === Default SQL macros call-throughs ===

    def get_columns_in_relation(self, relation: BaseRelation) -> List[Column]:
        """Default impl: call macro 'get_columns_in_relation' which is SQL-based"""
        return self.execute_macro('get_columns_in_relation', kwargs={'relation': relation})

    def list_relations_without_caching(self, schema_relation: BaseRelation) -> List[BaseRelation]:
        """Default impl: call macro 'list_relations_without_caching'"""
        result = self.execute_macro('list_relations_without_caching', kwargs={'schema_relation': schema_relation})
        return [self.Relation.create_from_row(self, row) for row in result]

    def create_schema(self, relation: BaseRelation) -> None:
        """Default impl: execute 'CREATE SCHEMA IF NOT EXISTS' macro"""
        self.execute_macro('create_schema', kwargs={'relation': relation})

    # ... ещё ~10 default implementations

SQLAdapter берёт абстрактные методы из BaseAdapter и реализует их через macro dispatch — вызов SQL macros (get_columns_in_relation, list_relations_without_caching, и т.д.). Эти macros определены в global_project/macros/adapters/.


Что значит “реализовать через macros”

Когда SQLAdapter вызывает self.execute_macro('get_columns_in_relation', ...) — это диспатч на Jinja macro с этим именем. Search order:

1. <adapter>__get_columns_in_relation  (например, postgres__get_columns_in_relation)
2. default__get_columns_in_relation

В dbt-adapters package есть default реализация:

-- dbt-adapters/dbt/include/global_project/macros/adapters/columns.sql
{% macro default__get_columns_in_relation(relation) %}
  {% call statement('get_columns_in_relation', fetch_result=True) %}
    SELECT
      column_name,
      data_type,
      character_maximum_length,
      numeric_precision,
      numeric_scale
    FROM {{ information_schema_name(relation.database) }}.columns
    WHERE table_schema = '{{ relation.schema }}'
      AND table_name = '{{ relation.identifier }}'
    ORDER BY ordinal_position
  {% endcall %}
  {{ return(load_result('get_columns_in_relation').table) }}
{% endmacro %}

Это ANSI SQL — работает на большинстве warehouses. dbt-postgres использует это by default.

dbt-snowflake переопределяет:

-- dbt-snowflake/dbt/include/snowflake/macros/adapters.sql
{% macro snowflake__get_columns_in_relation(relation) %}
  {%- set sql -%}
    DESCRIBE TABLE {{ relation }}
  {%- endset -%}
  {%- set result = run_query(sql) -%}
  -- Parse Snowflake-specific format
  {{ return(...) }}
{% endmacro %}

Snowflake-specific syntax DESCRIBE TABLE вместо information_schema.columns. Faster и более complete (показывает clustering keys, etc).


Когда подкласс SQLAdapter, когда BaseAdapter напрямую

SQLAdapter — если warehouse:

  • Работает через SQL statements (CREATE TABLE, SELECT, INSERT)
  • Использует cursor-based execution (DBI-style API)
  • Возвращает табличные результаты
  • Поддерживает information_schema или эквивалент

Примеры: Postgres, MySQL, SQLite, DuckDB, Snowflake, ClickHouse, Trino, Athena.

BaseAdapter напрямую — если:

  • Источник не SQL (REST API, NoSQL)
  • Используется DataFrame API (Spark, Polars, Arrow Flight)
  • Proprietary execution model
  • Сильно отличающиеся типы данных от SQL

Примеры:

  • dbt-spark — использует Spark DataFrame API через PySpark
  • dbt-bigquery — extends BaseAdapter для Arrow optimizations (хотя BigQuery — SQL)
  • Hypothetical dbt-mongodb — REST API + JSON documents
  • Hypothetical dbt-rest-api — gather data from REST endpoints

Иерархия классов в реальных adapter’ах

Class hierarchy in popular adapters
BaseAdapter (dbt-adapters)BaseAdapter — корень иерархии в dbt-adapters package. Все adapter'ы наследуют отсюда.
SQLAdapterSQLAdapter — большинство adapter'ов наследуют отсюда.

Снимок 2026 года:

  • Большинство adapter’ов extends SQLAdapter — Postgres, DuckDB, Snowflake, MySQL, ClickHouse.
  • BaseAdapter напрямую — Spark, BigQuery, некоторые специальные.
  • Полный список Trusted Adapters Program: https://docs.getdbt.com/docs/trusted-adapters

Минимальный SQLAdapter scaffold

Чтобы понять, что вам надо написать, посмотрим минимальный SQLAdapter для гипотетического warehouse myhouse:

# dbt-myhouse/dbt/adapters/myhouse/impl.py
from dbt.adapters.sql import SQLAdapter

from dbt.adapters.myhouse.connections import MyHouseConnectionManager
from dbt.adapters.myhouse.relation import MyHouseRelation
from dbt.adapters.myhouse.column import MyHouseColumn


class MyHouseAdapter(SQLAdapter):
    ConnectionManager = MyHouseConnectionManager
    Relation = MyHouseRelation
    Column = MyHouseColumn

    @classmethod
    def date_function(cls) -> str:
        return 'current_date'

    @classmethod
    def convert_text_type(cls, agate_table, col_idx):
        return 'TEXT'

    @classmethod
    def convert_number_type(cls, agate_table, col_idx):
        return 'DOUBLE'

    @classmethod
    def convert_boolean_type(cls, agate_table, col_idx):
        return 'BOOLEAN'

    @classmethod
    def convert_datetime_type(cls, agate_table, col_idx):
        return 'TIMESTAMP'

    @classmethod
    def convert_date_type(cls, agate_table, col_idx):
        return 'DATE'

    @classmethod
    def convert_time_type(cls, agate_table, col_idx):
        return 'TIME'

    def list_schemas(self, database: str) -> List[str]:
        """Override default (information_schema) with myhouse-specific"""
        return self.execute_macro('myhouse_list_schemas', kwargs={'database': database})

    # Most other methods inherit from SQLAdapter — no override needed

Дополнительно нужны три класса:

  1. MyHouseConnectionManager — управляет соединениями (открытие, exception handling, get_response). См. урок 4.
  2. MyHouseRelation — представляет relation в warehouse. См. урок 09/01.
  3. MyHouseColumn — представляет столбец. См. урок 09/02.

И profile schema:

  1. MyHouseCredentials — dataclass для profiles.yml. См. урок 3.

Эта четвёрка — минимальный набор для любого нового adapter’а.


Что наследуется автоматически

Из SQLAdapter (которого мы extend) приходят:

  • execute — выполнение SQL через cursor
  • add_query — выполнение одного query
  • get_columns_in_relation — через ANSI SQL information_schema (можно override)
  • list_relations_without_caching — через macro
  • create_schema / drop_schema — через macros
  • check_schema_exists — через macro
  • rename_relation — через ALTER … RENAME (можно override для warehouses без ALTER RENAME)
  • truncate_relation — через TRUNCATE TABLE
  • drop_relation — через DROP TABLE
  • current_timestamp — через CURRENT_TIMESTAMP

Это ~15 методов «бесплатно» из SQLAdapter. Если ваш warehouse — ANSI SQL — большинство работает out of box. Override нужен только для warehouse-specific особенностей.

NOTE

Если используете BaseAdapter напрямую — все эти методы нужно реализовать самому, потому что SQLAdapter их реализует через SQL macros, которые не работают для non-SQL источников.


Trusted Adapter Program

dbt Labs maintains программу для adapter’ов, которые ready для production:

Уровни Trust:

  1. Trusted by dbt Labs — официальные: dbt-postgres, dbt-redshift, dbt-snowflake, dbt-bigquery, dbt-spark, dbt-duckdb.

  2. Trusted Adapters Program — community adapters прошедшие проверку: dbt-databricks, dbt-trino, dbt-clickhouse, dbt-doris, dbt-firebolt, dbt-impala, и др.

  3. Community Adapters — без официальной проверки. Используйте на свой риск: dbt-singlestore, dbt-vertica, dbt-iotdb, и др.

Требования для Trusted Adapters Program:

  • Pass full dbt-tests-adapter suite (см. урок 09/05)
  • CI/CD setup, регулярные releases
  • Maintained by accountable owner (не abandoned)
  • Документация
  • Security review

Если пишете serious adapter — целитесь в Trusted Program. Это занимает месяцы, но дает credibility и community.


Попробуй сам

  1. Найдите файл dbt-adapters/dbt/adapters/sql/impl.py в site-packages.
  2. Откройте class SQLAdapter. Найдите все методы. Сравните с class BaseAdapter (наследуется от него).
  3. Откройте dbt-postgres/dbt/adapters/postgres/impl.py. Что override Postgres adapter по сравнению с SQLAdapter? (типы данных, информ. schema queries, transactional behaviors)
  4. Откройте dbt-bigquery/dbt/adapters/bigquery/impl.py. Заметьте, что extends BaseAdapter напрямую — потому что использует BigQuery Python client, не cursor.
  5. Bonus: создайте свой dbt-myhouse/dbt/adapters/myhouse/impl.py с минимальным scaffold выше (просто чтобы файл существовал). Запустите pip install -e . в той же папке. dbt должен распознать adapter (хотя не работать — нужны другие классы).

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

  1. BaseAdapter — корень иерархии. Абстрактный класс, минимум API. Используется для не-SQL источников (Spark, BigQuery, REST API).

  2. SQLAdapter — наследует BaseAdapter, реализует SQL-specific через macro dispatch. 95% adapter’ов наследуют отсюда (Postgres, DuckDB, Snowflake, MySQL, ClickHouse).

  3. Macro dispatch: SQLAdapter.get_columns_in_relation() -> diapatches to <adapter>__get_columns_in_relation Jinja macro -> falls back to default__get_columns_in_relation (ANSI SQL).

  4. Минимальный adapter = 4 класса: Adapter, ConnectionManager, Relation, Column + Credentials dataclass.

  5. Override только когда warehouse-specific — не дублируйте default. На каждый override должна быть причина (warehouse-specific syntax, optimization, missing feature).

  6. Trusted Adapter Program — путь к production-grade adapter. Месяцы работы + pass dbt-tests-adapter suite.

Проверка знанийKnowledge check
Senior хочет написать dbt adapter для warehouse, который работает через REST API (GET /tables, POST /query). Использовать SQLAdapter или BaseAdapter?
ОтветAnswer
**BaseAdapter напрямую**, не SQLAdapter.\n\n**Почему**:\n\n`SQLAdapter` предполагает SQL-based execution через cursor:\n\n```python\ndef add_query(self, sql: str, ...):\n conn = self.connections.get_thread_connection()\n cursor = conn.handle.cursor() # ← cursor-based API\n cursor.execute(sql) # ← execute SQL\n return conn, cursor\n```\n\nREST API не имеет cursor. Не имеет 'execute SQL'. Использует HTTP-вызовы.\n\nPlus, default implementation в SQLAdapter:\n\n```python\ndef get_columns_in_relation(self, relation):\n return self.execute_macro('get_columns_in_relation', ...)\n # -> SQL macro -> SELECT FROM information_schema.columns\n```\n\nЭто SQL query. REST API не имеет information_schema.\n\n**С BaseAdapter напрямую** вы получаете clean slate:\n\n```python\nclass RestApiAdapter(BaseAdapter):\n ConnectionManager = RestApiConnectionManager\n Relation = RestApiRelation\n Column = RestApiColumn\n \n def execute(self, sql, ...):\n # Override completely\n # Translate dbt's SQL to REST API calls\n response = self.connections.client.post('/query', json={'sql': sql})\n # Return as expected format\n ...\n \n def get_columns_in_relation(self, relation):\n # Override — use REST endpoint\n resp = self.connections.client.get(f'/tables/{relation.identifier}/columns')\n return [Column.from_dict(c) for c in resp.json()]\n \n def list_relations_without_caching(self, schema_relation):\n # Override\n resp = self.connections.client.get(f'/schemas/{schema_relation.schema}/tables')\n return [Relation.create(...) for t in resp.json()]\n \n # ... override все abstract methods\n```\n\n**Что нужно реализовать**:\n\n- `execute` — главная переписать.\n- `get_columns_in_relation` — fetch columns via API.\n- `list_relations_without_caching` — list endpoints.\n- `list_schemas` — list schemas.\n- `create_schema` / `drop_schema` — API calls.\n- `check_schema_exists` — HEAD request.\n- `rename_relation` — PATCH endpoint.\n- `drop_relation` — DELETE request.\n- Type conversions (`convert_text_type`, etc.) — map API types to dbt types.\n- Quoting policy — define для REST API (probably no quoting).\n\n**Что НЕ нужно**:\n\n- `add_query` — нет SQL\n- transaction management (`begin`, `commit`) — REST is stateless обычно\n- cursor-based result iteration\n\n**Materialization implications**:\n\nMaterializations в dbt предполагают SQL syntax (CREATE TABLE AS, INSERT). Для REST API вы должны:\n\n1. **Translate SQL to API**: parse model SQL, translate to API calls. Сложно для arbitrary SQL.\n\n2. **Restricted SQL subset**: документировать, что REST adapter поддерживает только simple SELECT, не arbitrary joins.\n\n3. **Custom materializations**: написать materialization которые используют API напрямую, не SQL.\n\n**Examples в природе**:\n\n- **dbt-singlestore-rest** — гипотетический adapter поверх SingleStore REST API.\n- **dbt-firebase-firestore** — REST-based access к Firestore.\n- **dbt-rest-api** (hypothetical) — universal REST adapter.\n\n**Production caveats**:\n\nREST-based adapters имеют ограничения:\n- Slower (HTTP overhead vs database protocol).\n- Limited SQL features.\n- Authentication через tokens (handle refresh).\n- Rate limits (handle 429 responses).\n\nЭто подходит для **specific niche** use cases, не для general production data warehouse.\n\nDecision: **BaseAdapter** для full control, без unnecessary SQL assumptions.\n\nЭто описано в Adapter Program documentation и реальных examples от community.
Проверка знанийKnowledge check
При написании adapter для нового SQL warehouse (например, OceanBase), что override обязательно vs что наследовать from SQLAdapter?
ОтветAnswer
Senior должен **override only what's necessary**. Не дублировать default behavior.\n\n**Обязательно override**:\n\n**1. Class-level attributes**:\n\n```python\nclass OceanBaseAdapter(SQLAdapter):\n ConnectionManager = OceanBaseConnectionManager # ← always\n Relation = OceanBaseRelation # ← always\n Column = OceanBaseColumn # ← always\n```\n\nЭто **identity** вашего adapter. Без них dbt не знает, какие классы использовать.\n\n**2. Type conversion methods**:\n\n```python\n@classmethod\ndef convert_text_type(cls, agate_table, col_idx):\n return 'TEXT' # или 'VARCHAR', 'CLOB', etc. — depends на warehouse\n\n@classmethod\ndef convert_number_type(cls, agate_table, col_idx):\n # OceanBase-specific: BIGINT, NUMBER, DECIMAL?\n return 'BIGINT'\n\n@classmethod\ndef convert_datetime_type(cls, agate_table, col_idx):\n return 'TIMESTAMP' # или 'DATETIME'\n```\n\nЭти methods используются `dbt seed` для CSV -> SQL types mapping. Default использует ANSI types, но warehouse может иметь свои.\n\n**3. date_function**:\n\n```python\n@classmethod\ndef date_function(cls) -> str:\n return 'current_date' # или 'CURDATE()' (MySQL-style), 'current_date()' (Snowflake)\n```\n\nИспользуется в snapshots для current date.\n\n**Часто override**:\n\n**4. list_schemas** — если warehouse не использует standard information_schema:\n\n```python\ndef list_schemas(self, database: str) -> List[str]:\n return self.execute_macro('oceanbase_list_schemas', kwargs={'database': database})\n```\n\n**5. get_columns_in_relation** — если SHOW COLUMNS / DESCRIBE TABLE даёт более complete info:\n\n```python\ndef get_columns_in_relation(self, relation):\n # Default использует information_schema.columns\n # Override если warehouse имеет лучший syntax\n return self.execute_macro('oceanbase__get_columns_in_relation', kwargs={'relation': relation})\n```\n\nИ соответствующий macro в `macros/adapters.sql`:\n\n```jinja\n{% macro oceanbase__get_columns_in_relation(relation) %}\n {% call statement('get_columns_in_relation', fetch_result=True) %}\n SHOW COLUMNS FROM {{ relation }}\n {% endcall %}\n ...\n{% endmacro %}\n```\n\n**Опционально override**:\n\n**6. rename_relation** — если warehouse не поддерживает ALTER TABLE RENAME:\n\n```python\ndef rename_relation(self, from_relation, to_relation):\n # Default — ALTER TABLE RENAME\n # Override если warehouse использует RENAME TABLE (MySQL syntax)\n self.execute_macro('rename_relation', kwargs={'from_relation': from_relation, 'to_relation': to_relation})\n```\n\n**7. truncate_relation** — если TRUNCATE не поддерживается:\n\n```python\ndef truncate_relation(self, relation):\n # Default — TRUNCATE TABLE\n # Override если используется DELETE FROM\n ...\n```\n\n**8. cancel_open_connections** — если warehouse имеет специфический cancel mechanism.\n\n**НЕ override (наследовать)**:\n\n**9. execute** — default через SQLAdapter работает для cursor-based.\n\n**10. add_query** — generic.\n\n**11. get_response** — default returns AdapterResponse.\n\n**12. get_result_from_cursor** — generic iteration.\n\n**13. quote** — default обычно works (можно override для proprietary quoting).\n\n**Decision tree для override**:\n\n```\nFor each method in SQLAdapter:\n Does warehouse behave differently from default?\n No -> don't override\n Yes -> \n Is the difference syntactic? (e.g., "DATE_TRUNC" vs "TRUNC")\n -> Override the macro, not the Python method\n Is the difference structural? (e.g., result format different)\n -> Override Python method\n```\n\n**Practical workflow**:\n\n1. **Start minimal**: ConnectionManager, Relation, Column, type conversions, date_function.\n2. **Run dbt-tests-adapter** suite (см. урок 09/05).\n3. **Find failures**: тесты покажут где default не работает для вашего warehouse.\n4. **Override targeted**: override only failed methods.\n5. **Iterate**: re-run tests, add overrides.\n\nЭто **test-driven adapter development**.\n\n**Anti-patterns**:\n\n- **Override everything**: дублируйте default code. Maintenance hell.\n- **Override без причины**: 'might be different' — let tests prove necessity.\n- **Copy-paste от другого adapter**: legacy code, может быть outdated.\n\nРеферат: **inherit from SQLAdapter, override target methods after testing**. Это **discipline of senior**.

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

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

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

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