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’ов:
Простое правило: если ваш 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’ах
Снимок 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
Дополнительно нужны три класса:
MyHouseConnectionManager— управляет соединениями (открытие, exception handling, get_response). См. урок 4.MyHouseRelation— представляет relation в warehouse. См. урок 09/01.MyHouseColumn— представляет столбец. См. урок 09/02.
И profile schema:
MyHouseCredentials— dataclass для profiles.yml. См. урок 3.
Эта четвёрка — минимальный набор для любого нового adapter’а.
Что наследуется автоматически
Из SQLAdapter (которого мы extend) приходят:
execute— выполнение SQL через cursoradd_query— выполнение одного queryget_columns_in_relation— через ANSI SQL information_schema (можно override)list_relations_without_caching— через macrocreate_schema/drop_schema— через macroscheck_schema_exists— через macrorename_relation— через ALTER … RENAME (можно override для warehouses без ALTER RENAME)truncate_relation— через TRUNCATE TABLEdrop_relation— через DROP TABLEcurrent_timestamp— через CURRENT_TIMESTAMP
Это ~15 методов «бесплатно» из SQLAdapter. Если ваш warehouse — ANSI SQL — большинство работает out of box. Override нужен только для warehouse-specific особенностей.
Если используете BaseAdapter напрямую — все эти методы нужно реализовать самому, потому что SQLAdapter их реализует через SQL macros, которые не работают для non-SQL источников.
Trusted Adapter Program
dbt Labs maintains программу для adapter’ов, которые ready для production:
Уровни Trust:
-
Trusted by dbt Labs — официальные: dbt-postgres, dbt-redshift, dbt-snowflake, dbt-bigquery, dbt-spark, dbt-duckdb.
-
Trusted Adapters Program — community adapters прошедшие проверку: dbt-databricks, dbt-trino, dbt-clickhouse, dbt-doris, dbt-firebolt, dbt-impala, и др.
-
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.
Попробуй сам
- Найдите файл
dbt-adapters/dbt/adapters/sql/impl.pyв site-packages. - Откройте
class SQLAdapter. Найдите все методы. Сравните сclass BaseAdapter(наследуется от него). - Откройте
dbt-postgres/dbt/adapters/postgres/impl.py. Что override Postgres adapter по сравнению с SQLAdapter? (типы данных, информ. schema queries, transactional behaviors) - Откройте
dbt-bigquery/dbt/adapters/bigquery/impl.py. Заметьте, что extends BaseAdapter напрямую — потому что использует BigQuery Python client, не cursor. - Bonus: создайте свой
dbt-myhouse/dbt/adapters/myhouse/impl.pyс минимальным scaffold выше (просто чтобы файл существовал). Запуститеpip install -e .в той же папке. dbt должен распознать adapter (хотя не работать — нужны другие классы).
Ключевые выводы
-
BaseAdapter — корень иерархии. Абстрактный класс, минимум API. Используется для не-SQL источников (Spark, BigQuery, REST API).
-
SQLAdapter — наследует BaseAdapter, реализует SQL-specific через macro dispatch. 95% adapter’ов наследуют отсюда (Postgres, DuckDB, Snowflake, MySQL, ClickHouse).
-
Macro dispatch: SQLAdapter.get_columns_in_relation() -> diapatches to
<adapter>__get_columns_in_relationJinja macro -> falls back todefault__get_columns_in_relation(ANSI SQL). -
Минимальный adapter = 4 класса: Adapter, ConnectionManager, Relation, Column + Credentials dataclass.
-
Override только когда warehouse-specific — не дублируйте default. На каждый override должна быть причина (warehouse-specific syntax, optimization, missing feature).
-
Trusted Adapter Program — путь к production-grade adapter. Месяцы работы + pass dbt-tests-adapter suite.