Learning Platform
Глоссарий Troubleshooting
Урок 15.03 · 35 мин
Продвинутый
dbt adapterCredentialsConnectionManagerdbt-tests-adapter

Путь B: Свой dbt adapter from scratch

Если вы выбрали этот путь — это самый technically demanding capstone. Цель — написать functional dbt adapter from scratch, который passes dbt-tests-adapter suite. Realistic timeline: 30-40 часов.

Что вы получите:

  • Глубокое понимание dbt internals на уровне «писал сам».
  • Реальный артефакт — installable Python package.
  • Potentially useful для community (если warehouse не имеет first-class support).
  • Portfolio piece, который однозначно демонстрирует senior level.

В этом уроке мы пройдём процесс создания dbt-sqlite (как референс) — простейший viable adapter для local dev и learning. Вы можете применить тот же подход к dbt-pglite, dbt-clickhouse-light, или собственному target warehouse.


pyproject.toml — PEP 517/518/621; build backends; tomllib

Step 1: Choose target warehouse

Выбор warehouse

В rest этого урока примеры на dbt-sqlite. Same techniques применимы к любой warehouse.


Step 2: Setup adapter project

dbt Labs provides cookiecutter template:

pip install cookiecutter
cookiecutter https://github.com/dbt-labs/dbt-adapter-cookiecutter

Interactive prompts:

project_name: dbt-sqlite
author_name: Your Name
author_email: [email protected]
github_username: yourname
adapter_name: sqlite
support_python_version: 3.10

Generates structure:

dbt-sqlite/
├── pyproject.toml
├── README.md
├── LICENSE
├── .github/workflows/   # CI templates
├── dbt/adapters/sqlite/
│   ├── __init__.py
│   ├── connections.py    # ConnectionManager
│   ├── impl.py           # SQLiteAdapter (Adapter API)
│   ├── credentials.py    # SQLiteCredentials
│   └── relation.py       # SQLiteRelation
├── dbt/include/sqlite/
│   ├── macros/           # adapter-specific macros
│   ├── profile_template.yml
│   └── dbt_project.yml
└── tests/
    ├── functional/       # dbt-tests-adapter integration tests
    └── unit/             # internal tests
cd dbt-sqlite
pip install -e ".[dev]"

Step 3: Credentials class

dbt/adapters/sqlite/credentials.py:

from dataclasses import dataclass
from typing import Optional

from dbt.adapters.contracts.connection import Credentials


@dataclass
class SQLiteCredentials(Credentials):
    """SQLite adapter credentials."""
    
    # Required fields (defined by Credentials base):
    # database: str
    # schema: str
    
    # SQLite-specific:
    database_path: str = ":memory:"
    
    # Override defaults
    threads: int = 1
    
    ALIASES = {
        "db_path": "database_path",   # alternative name
        "path": "database_path",       # short form
    }
    
    @property
    def type(self) -> str:
        return "sqlite"
    
    @property
    def unique_field(self) -> str:
        return self.database_path
    
    def _connection_keys(self) -> tuple:
        """Fields shown в `dbt debug` output."""
        return ("database", "schema", "database_path", "threads")

Что важно:

  1. @dataclass — Python data class.
  2. Inheritance from Credentials — base class в dbt.adapters.contracts.connection.
  3. ALIASES — alternative field names users can use в profiles.yml.
  4. type property — adapter name.
  5. unique_field — какое поле identifies unique connection (для caching).
  6. _connection_keys — fields shown в dbt debug output.

Step 4: ConnectionManager

dbt/adapters/sqlite/connections.py:

import sqlite3
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Optional

from dbt.adapters.contracts.connection import (
    AdapterResponse,
    Connection,
    ConnectionState,
)
from dbt.adapters.sql import SQLConnectionManager
from dbt.adapters.sqlite.credentials import SQLiteCredentials


@dataclass
class SQLiteAdapterResponse(AdapterResponse):
    pass


class SQLiteConnectionManager(SQLConnectionManager):
    TYPE = "sqlite"
    
    @classmethod
    def open(cls, connection: Connection) -> Connection:
        if connection.state == ConnectionState.OPEN:
            return connection
        
        credentials: SQLiteCredentials = connection.credentials
        
        try:
            handle = sqlite3.connect(
                credentials.database_path,
                isolation_level=None,  # autocommit mode
            )
            handle.execute("PRAGMA foreign_keys = ON")
            
            connection.handle = handle
            connection.state = ConnectionState.OPEN
            return connection
        except Exception as e:
            connection.state = ConnectionState.FAIL
            raise dbt.exceptions.FailedToConnectError(str(e))
    
    @classmethod
    def get_response(cls, cursor) -> SQLiteAdapterResponse:
        rows_affected = cursor.rowcount if cursor.rowcount > 0 else 0
        return SQLiteAdapterResponse(
            _message=f"SUCCESS {rows_affected}",
            rows_affected=rows_affected,
        )
    
    def cancel(self, connection: Connection):
        """SQLite doesn't support cancellation."""
        pass
    
    @contextmanager
    def exception_handler(self, sql: str):
        try:
            yield
        except sqlite3.DatabaseError as e:
            raise dbt.exceptions.DatabaseError(str(e))
        except Exception as e:
            raise dbt.exceptions.RuntimeError(str(e))

Key methods:

  1. open — establishes connection. Returns connection с state OPEN.
  2. get_response — после query execution, returns AdapterResponse with metadata (rows affected).
  3. cancel — cancel running query (SQLite doesn’t support, no-op).
  4. exception_handler — context manager wrapping query execution, translates exceptions.

Step 5: Adapter implementation (impl.py)

dbt/adapters/sqlite/impl.py:

from typing import List, Optional

from dbt.adapters.base.column import Column
from dbt.adapters.base.impl import BaseAdapter
from dbt.adapters.base.relation import BaseRelation
from dbt.adapters.sql import SQLAdapter
from dbt.adapters.sqlite.connections import SQLiteConnectionManager
from dbt.adapters.sqlite.credentials import SQLiteCredentials
from dbt.adapters.sqlite.relation import SQLiteRelation


class SQLiteAdapter(SQLAdapter):
    ConnectionManager = SQLiteConnectionManager
    Relation = SQLiteRelation
    
    @classmethod
    def date_function(cls) -> str:
        return "CURRENT_TIMESTAMP"
    
    @classmethod
    def is_cancelable(cls) -> bool:
        return False  # SQLite не cancel
    
    def list_schemas(self, database: str) -> List[str]:
        # SQLite не имеет multiple schemas — return single
        return ["main"]
    
    def check_schema_exists(self, database: str, schema: str) -> bool:
        return schema == "main"
    
    def drop_schema(self, relation: BaseRelation) -> None:
        # SQLite — no schemas, no-op
        pass
    
    def create_schema(self, relation: BaseRelation) -> None:
        # SQLite — no schemas, no-op
        pass

SQLAdapter extends BaseAdapter для SQL-based warehouses. Provides default implementations для quoting, identifier resolution, etc.


Step 6: Relation class

dbt/adapters/sqlite/relation.py:

from dataclasses import dataclass

from dbt.adapters.base.relation import BaseRelation, Policy


@dataclass
class SQLiteQuotePolicy(Policy):
    database: bool = False
    schema: bool = False
    identifier: bool = True


@dataclass
class SQLiteIncludePolicy(Policy):
    database: bool = False
    schema: bool = False
    identifier: bool = True


@dataclass(frozen=True, eq=False, repr=False)
class SQLiteRelation(BaseRelation):
    quote_policy: Policy = SQLiteQuotePolicy()
    include_policy: Policy = SQLiteIncludePolicy()

Relation represents database object (table, view, etc.) с naming and quoting rules. SQLite — no databases или schemas, only identifiers (table names).


Step 7: Required macros

dbt expects 20+ macros в dbt/include/sqlite/macros/.

dbt/include/sqlite/macros/adapters.sql:

{% macro sqlite__get_columns_in_relation(relation) -%}
  {% call statement('get_columns_in_relation', fetch_result=True) %}
    SELECT
      name AS column_name,
      type AS data_type,
      0 AS character_maximum_length,
      0 AS numeric_precision,
      0 AS numeric_scale
    FROM pragma_table_info('{{ relation.identifier }}')
    ORDER BY cid
  {% endcall %}
  {% set table = load_result('get_columns_in_relation').table %}
  {{ return(sql_convert_columns_in_relation(table)) }}
{%- endmacro %}


{% macro sqlite__list_relations_without_caching(schema_relation) -%}
  {% call statement('list_relations', fetch_result=True) %}
    SELECT
      'main' AS database,
      name AS identifier,
      'main' AS schema,
      CASE 
        WHEN type = 'table' THEN 'table'
        WHEN type = 'view' THEN 'view'
        ELSE 'table'
      END AS table_type
    FROM sqlite_master
    WHERE type IN ('table', 'view')
  {% endcall %}
  {{ return(load_result('list_relations').table) }}
{%- endmacro %}


{% macro sqlite__create_view_as(relation, sql) -%}
  CREATE VIEW {{ relation.identifier }} AS {{ sql }}
{%- endmacro %}


{% macro sqlite__create_table_as(temporary, relation, sql) -%}
  CREATE TABLE {{ relation.identifier }} AS {{ sql }}
{%- endmacro %}


{% macro sqlite__drop_relation(relation) -%}
  DROP {{ relation.type }} IF EXISTS {{ relation.identifier }}
{%- endmacro %}


{% macro sqlite__rename_relation(from_relation, to_relation) -%}
  ALTER TABLE {{ from_relation.identifier }} RENAME TO {{ to_relation.identifier }}
{%- endmacro %}


{% macro sqlite__truncate_relation(relation) -%}
  DELETE FROM {{ relation.identifier }}
{%- endmacro %}


{% macro sqlite__current_timestamp() -%}
  CURRENT_TIMESTAMP
{%- endmacro %}

Это минимум для basic functionality. Полный list adapter macros — около 25-30. Полный список в dbt-core/include/global_project/macros/adapters/.


Step 8: Profile template

dbt/include/sqlite/profile_template.yml:

fixed:
  type: sqlite

prompts:
  database_path:
    hint: "path to your SQLite database file"
  threads:
    hint: "number of threads (typically 1 for SQLite due to single-writer)"
    default: 1
    type: int

This guides dbt init setup process when user creates new project.


Step 9: Tests — dbt-tests-adapter

dbt Labs provides standardized adapter test suite — dbt-tests-adapter:

pip install dbt-tests-adapter

tests/functional/conftest.py:

import pytest


@pytest.fixture(scope="class")
def dbt_profile_target():
    return {
        "type": "sqlite",
        "threads": 1,
        "database": "test_db",
        "schema": "main",
        "database_path": ":memory:",
    }

tests/functional/adapter/test_basic.py:

import pytest

from dbt.tests.adapter.basic.test_base import BaseSimpleMaterializations
from dbt.tests.adapter.basic.test_singular_tests import BaseSingularTests
from dbt.tests.adapter.basic.test_singular_tests_ephemeral import BaseSingularTestsEphemeral
from dbt.tests.adapter.basic.test_empty import BaseEmpty
from dbt.tests.adapter.basic.test_ephemeral import BaseEphemeral
from dbt.tests.adapter.basic.test_incremental import BaseIncremental
from dbt.tests.adapter.basic.test_generic_tests import BaseGenericTests


class TestSimpleMaterializationsSQLite(BaseSimpleMaterializations):
    pass


class TestSingularTestsSQLite(BaseSingularTests):
    pass


class TestEmptySQLite(BaseEmpty):
    pass


class TestEphemeralSQLite(BaseEphemeral):
    pass


class TestIncrementalSQLite(BaseIncremental):
    pass


class TestGenericTestsSQLite(BaseGenericTests):
    pass

Run tests:

pytest tests/functional/ -v

Initial run — почти все fail. Это normal. You’ll fix one at a time.


Step 10: Iterate, fix failures

pytest tests/functional/adapter/test_basic.py::TestSimpleMaterializationsSQLite -v

Failure example:

FAILED tests/functional/adapter/test_basic.py::TestSimpleMaterializationsSQLite::test_base
    AttributeError: SQLiteAdapter has no attribute 'create_table'

Add the method to impl.py:

class SQLiteAdapter(SQLAdapter):
    # ... existing code ...
    
    def create_table(self, relation: BaseRelation, sql: str):
        sql = f"CREATE TABLE {relation.identifier} AS {sql}"
        self.execute(sql, auto_begin=False)

Re-run, fix next failure. Iterate.

This is most time-consuming step. Realistic ~15-20 часов.


Step 11: Custom macros for SQLite quirks

SQLite has limitations vs full SQL warehouses. Handle these в adapter macros:

Quirk 1: SQLite не supports INFORMATION_SCHEMA. Use pragma_table_info instead (already в get_columns_in_relation).

Quirk 2: SQLite не supports schemas. Override all schema-related macros to be no-ops.

Quirk 3: SQLite uses || для concatenation, не CONCAT(). Override concat():

{% macro sqlite__concat(fields) -%}
    {{ fields|join(' || ') }}
{%- endmacro %}

Quirk 4: SQLite date functions different. Override:

{% macro sqlite__dateadd(datepart, interval, from_date_or_timestamp) %}
    DATETIME({{ from_date_or_timestamp }}, '+{{ interval }} {{ datepart }}')
{% endmacro %}

Полный list quirks специфичен warehouse. Common — найти в other community adapters.


Step 12: Package and publish

pyproject.toml:

[project]
name = "dbt-sqlite"
version = "0.1.0"
description = "dbt adapter for SQLite"
readme = "README.md"
requires-python = ">=3.10"
authors = [{name = "Your Name", email = "[email protected]"}]
dependencies = [
    "dbt-adapters>=1.0.0",
    "dbt-common>=1.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest",
    "dbt-tests-adapter",
]

[project.urls]
Repository = "https://github.com/yourname/dbt-sqlite"

Build и (optionally) publish:

# Build
pip install build
python -m build

# Test install locally
pip install dist/dbt_sqlite-0.1.0-py3-none-any.whl

# Use в a dbt project
dbt init test_project --adapter sqlite

# (Optional) Publish to PyPI
pip install twine
twine upload dist/*

Step 13: Trusted Adapter Program (опционально)

Если хотите ваш adapter formally recognized:

  1. Read Trusted Adapter Program на dbt docs.
  2. Requirements:
    • Public GitHub repo.
    • CI с dbt-tests-adapter passing.
    • Documentation README с setup, usage examples.
    • Active maintainer (you).
    • License compatible (Apache 2.0 typically).
  3. Submit application via dbt-labs/trusted-adapters repo.
  4. Wait for review (weeks to months).
  5. If approved — your adapter listed на dbt docs site.

This dramatic visibility boost. Even community-trusted (без formal program) adapters get usage.


Common challenges

Гранвая части adapter development

Minimum viable adapter checklist

To complete capstone, adapter должен:

  • Pass BaseSimpleMaterializations test class.
  • Pass BaseEmpty test class.
  • Pass BaseSingularTests test class.
  • Have working dbt init flow с profile template.
  • Support dbt debug (validate connection).
  • Support dbt run для simple model.
  • GitHub repo с README, CI/CD.
  • Tests в repo (passing).
  • Installable via pip from your repo.

Stretch goals:

  • Pass full dbt-tests-adapter suite.
  • Support incremental materializations.
  • Support snapshots.
  • PyPI publication.
  • Trusted Adapter Program application.

Проверка знанийKnowledge check
После 20 часов work на dbt-sqlite, проходят TestSimpleMaterializations, TestEmpty, TestSingularTests. Но TestIncremental fails с error 'sqlite3.OperationalError: Cannot add a NOT NULL column with default value NULL'. Что делать?
ОтветAnswer
Это classic adapter development issue — **warehouse-specific DDL limitations**. Понимание — фундамент для anyone debugging adapter issues. **Что произошло:** dbt incremental materialization works by: 1. Checking если table exists. 2. If yes — run incremental merge logic. 3. If new columns в model SQL but not в existing table -> `ALTER TABLE ADD COLUMN`. Specifically: ```sql ALTER TABLE fct_orders ADD COLUMN new_column INTEGER NOT NULL ``` **SQLite limitation:** unlike Postgres/Snowflake, SQLite **не allows** `ALTER TABLE ADD COLUMN ... NOT NULL` without a default value. Reason — SQLite не can fill existing rows. **Workaround в SQLite:** rebuild table: ```sql -- Create new table с desired schema CREATE TABLE fct_orders_new AS SELECT *, NULL AS new_column -- nullable, fill with NULL для existing rows FROM fct_orders; -- Drop old table DROP TABLE fct_orders; -- Rename new ALTER TABLE fct_orders_new RENAME TO fct_orders; -- (Recreate indexes if any) ``` This is **destructive** but only viable approach в SQLite. **Implementation в dbt adapter:** Need override default `get_alter_column_type_sql` или write custom macro для column addition. ```sql -- dbt/include/sqlite/macros/incremental.sql {% macro sqlite__alter_relation_add_remove_columns(relation, add_columns, remove_columns) %} {% if add_columns is none %} {% set add_columns = [] %} {% endif %} {% if remove_columns is none %} {% set remove_columns = [] %} {% endif %} {# SQLite не supports ALTER TABLE ADD COLUMN NOT NULL без default #} {# Approach: rebuild table #} {% if add_columns or remove_columns %} {# Get all existing columns minus removed #} {%- set existing_columns = adapter.get_columns_in_relation(relation) -%} {%- set columns_to_keep = [] -%} {%- for col in existing_columns -%} {%- if col.name not in remove_columns -%} {%- set _ = columns_to_keep.append(col) -%} {%- endif -%} {%- endfor -%} {# Build SELECT для new table #} {%- set select_parts = [] -%} {%- for col in columns_to_keep -%} {%- set _ = select_parts.append('"' ~ col.name ~ '"') -%} {%- endfor -%} {%- for col in add_columns -%} {%- set _ = select_parts.append('NULL AS "' ~ col.name ~ '"') -%} {%- endfor -%} {# Rebuild via temp table #} {%- set temp_name = relation.identifier ~ '_temp_rebuild' -%} {% call statement('rebuild_table_step1') %} CREATE TABLE {{ temp_name }} AS SELECT {{ select_parts | join(', ') }} FROM {{ relation }} {% endcall %} {% call statement('rebuild_table_step2') %} DROP TABLE {{ relation }} {% endcall %} {% call statement('rebuild_table_step3') %} ALTER TABLE {{ temp_name }} RENAME TO {{ relation.identifier }} {% endcall %} {% endif %} {% endmacro %} ``` Now: - New columns added with NULL default (nullable). - Existing data preserved (copied к new table). - dbt's incremental logic works. **Trade-off:** Rebuilding table — **expensive** на big data. SQLite не designed для huge data, so this acceptable. For warehouse supporting fast ALTER TABLE (Snowflake/BigQuery) — default behavior уже good. **Re-run tests:** ```bash pytest tests/functional/adapter/test_basic.py::TestIncrementalSQLite -v ``` Should now pass. **Lessons learned:** 1. **Adapter development = warehouse-specific quirks accumulation.** Each warehouse имеет gotcha. SQLite (limited ALTER), MySQL (no MERGE), Redshift (no JSON в group by). Adapter devs accumulate "tricks" through trial-and-error. 2. **Read existing adapter source code.** github.com/dbt-labs/dbt-postgres, github.com/dbt-labs/dbt-snowflake — see how they handle quirks. Patterns translatable. 3. **Issues документировать.** Ин README или CHANGELOG: "SQLite does not support ALTER TABLE ADD COLUMN NOT NULL. Adapter handles by rebuilding table — slow for big data." Users know limitation upfront. 4. **Tests catches real bugs.** TestIncremental — would never have found this manually. Standard test suite invaluable. 5. **Performance characteristics differ.** What's fast в Snowflake (ALTER TABLE ADD COLUMN — O(1)) is slow в SQLite (rebuild table — O(N) rows). Adapter docs should note these. **For capstone:** Document в README: ```markdown ## Limitations SQLite doesn't support `ALTER TABLE ADD COLUMN NOT NULL` без default. This adapter handles by rebuilding the table. For large datasets, ALTER operations may be slow. Known limitations: - No concurrent writers (SQLite restriction). - ALTER TABLE rebuilds entire table. - No native JSON support (uses TEXT). Workarounds: - For incremental с many new columns, prefer full refresh. - For high-concurrency scenarios, use different warehouse. ``` This honest documentation valued. **Если adapter shipping к users:** Issue: ```markdown # Known limitation: ALTER TABLE behavior SQLite не support ALTER TABLE ADD COLUMN NOT NULL без default value. This adapter mitigates by rebuilding the table. The operation is slow for large tables. For production use cases with frequent schema changes, рассмотрите другой warehouse. Workaround for users: - Use `--full-refresh` flag (rebuilds entire table; same as adapter does automatically). - Avoid adding NOT NULL columns to incremental models. This is не bug — это inherent SQLite limitation. ``` Users grateful когда issues explained. **Главный урок:** **Adapter development = continuous discovery warehouse limitations**. Plan для это. Don't expect linear development. Each test failure -> research -> workaround -> fix. 20 часов into adapter может uncover next 10 hours of edge cases. Это normal. Senior dev = pattern matches к existing adapter solutions, documents limitations honestly, ships maintainable adapter.

Проверка знанийKnowledge check
Adapter готов на 90%, passes most tests, но 3-4 failing tests deeply tricky. Сколько времени spend trying to fix vs. ship 'mostly-working' adapter?
ОтветAnswer
Этой decision faces every adapter developer. Понимание — критично для shipping software vs. perpetually polishing. **80-20 rule applies.** 80% completion typically takes 20% of total effort. Last 20% takes 80%. Адресс this realism: - 90% completion = 30-40 hours (your current). - 95% completion = 50-60 hours (next 10% = 20+ hours). - 99% completion = 80-100 hours. **Question:** worth additional 30 hours для last 5%? Depends: **If adapter is hobby/learning project (most cases):** Ship **at 90%**. Document what works, what не yet. Users can use working parts. Reasons: 1. **Diminishing returns.** Last 5% — niche edge cases. Most users never hit them. 2. **Burnout risk.** 30 more hours грind может kill enthusiasm. Better to ship и iterate later. 3. **Real-world feedback.** Once shipped, users tell you what they need. Better signal than guessing. 4. **Portfolio value.** "Shipped working adapter" >>> "shipped polished adapter eventually". 5. **Time opportunity cost.** 30 hours could be new project, blog posts, conference talks. **If adapter is для production team:** Different calculus. Bugs in production = real impact. Investment в reliability pays off. Decision factors: - How critical is each failing test? - Can workaround documented для users? - How many users affected? For critical failures (e.g., incremental tests fail) — fix. For edge cases (e.g., snapshot timestamp strategy precision issue) — ship с documented limitation, fix later. **Specific approaches:** **Approach 1: Skip failing tests, ship и document.** ```python # tests/functional/adapter/test_basic.py class TestSimpleMaterializationsSQLite(BaseSimpleMaterializations): pass # passing class TestIncrementalSQLite(BaseIncremental): @pytest.mark.skip(reason="ALTER TABLE limitations — известная issue, см. README") def test_specific_problem(self): pass class TestSnapshotStrategyCheckSQLite(BaseSnapshotCheck): pytestmark = pytest.mark.skip( reason="snapshot check strategy fails on float precision — \n" "known SQLite limitation. Workaround: use timestamp strategy." ) ``` И в README: ```markdown ## Limitations ### Known issues - Snapshot check strategy may produce false positives on float comparisons. Workaround: use timestamp strategy. - ALTER TABLE ADD COLUMN NOT NULL не supported (SQLite limitation). Adapter rebuilds table — slow for large data. - Other warehouses recommended для production scale. ### Test status Passing: BaseSimpleMaterializations, BaseEmpty, BaseEphemeral, BaseSingularTests, BaseGenericTests, partial BaseIncremental. Known failures (skipped): TestSnapshotCheck, full TestIncremental. For production, рассмотрите dbt-postgres или dbt-duckdb. ``` Honest scope communication. Better чем pretending tests pass. **Approach 2: Mark adapter "beta" с clear scope.** Package в pyproject.toml: ```toml [project] name = "dbt-sqlite" version = "0.1.0-beta" [project.urls] Documentation = "https://github.com/yourname/dbt-sqlite#known-limitations" ``` Version 0.1.0-beta тон — "не production-ready, learning project". Sets expectations. **Approach 3: Defer hard tests, ship in v0.1, fix в v0.2.** Roadmap: ```markdown ## Roadmap ### v0.1 (current) — Basic functionality - [x] Simple materializations (table, view) - [x] Ephemeral models - [x] Generic tests - [x] Singular tests - [ ] Full incremental (partial) - [ ] Snapshots (partial) ### v0.2 (planned) — Production ready - Full incremental (handle column addition) - Snapshot timestamp strategy ### v0.3 (future) — Stretch goals - Custom materializations - DuckLake support - Performance optimizations ``` Users know what's stable, what's coming. Set realistic expectations. **Approach 4: Ship и invite contributions.** Issues tracker открыт. CONTRIBUTING.md says: "PRs welcome для failing tests." ```markdown ## Contributing Кogда tests failing: - TestIncremental/test_X — needs investigation в ALTER TABLE handling. - TestSnapshotCheck/test_Y — float precision issue. Good first issue для community contributors. Mark помечены с `help wanted` label. ``` This open source approach. Maintain adapter с community help. **Where to draw the line:** My heuristic для shipping at 90%: 1. **All "basic" tests pass** (TestSimpleMaterializations, TestEmpty, TestSingularTests, TestGenericTests). 2. **'dbt init', 'dbt debug', 'dbt run' для simple model — work**. 3. **'dbt build' для basic project — works**. 4. **Документация honest о limitations**. 5. **PyPI publishable** (even if 0.1.0-beta). If above — **ship**. Real users provide better signal than perfect tests. **Don't ship if:** 1. `dbt debug` fails. (Cannot even connect.) 2. `dbt run` fails for trivial model. (Cannot do simplest thing.) 3. All advanced tests fail (not just edge cases — pattern failures). 4. README doesn't acknowledge limitations. **For capstone purposes:** Shipping at 90% — completely valid capstone outcome. Deliverable: - GitHub repo с adapter код. - README explaining what works. - Tests showing passing percentage (e.g., "27/35 tests passing"). - Documented limitations. - pip installable. - Maybe even PyPI published. Portfolio framing: "Built dbt-sqlite adapter from scratch — implements Credentials, ConnectionManager, 25+ adapter macros. Passes core dbt-tests-adapter suite (27/35 tests passing). Documented limitations и workarounds. Apache 2.0 open source — github.com/yourname/dbt-sqlite." This honestly framed accomplishment > "passes all tests" lie. **Главный урок:** **Ship at 80-90% complete, iterate based on feedback.** Perfectionism kills projects. Real-world software always has known issues (look at dbt-core itself, dbt-snowflake, any open source — issues tracker full). What matters: it works для intended cases, limitations documented, easy для users to assess fit. Take pride в honest scope, not pretended completeness. Ship the adapter at 90%. Document limitations. Iterate based на user feedback. This is **senior engineering pragmatism**.

Итого

  1. Path B capstone = functional adapter, passes core dbt-tests-adapter tests.
  2. Choose simple warehouse для first adapter: SQLite recommended.
  3. Setup: cookiecutter template + hatch для dev environment.
  4. Implement core classes: Credentials, ConnectionManager, Adapter, Relation.
  5. Write required macros (25+) для adapter-specific behavior.
  6. dbt-tests-adapter suite — iterate, fix test failures one at a time.
  7. Custom macros для warehouse quirks (DDL limitations, case sensitivity, etc.).
  8. Package и publish через pyproject.toml + PyPI (optional).
  9. Trusted Adapter Program (optional) для formal recognition.
  10. Common challenges: macro namespace conventions, dbt-tests-adapter version compatibility, DBAPI 2.0 differences, transaction handling, cache invalidation.
  11. Ship at 80-90% с honest documentation. Don’t perfectionism kill project.
  12. Realistic timeline: 30-40 часов для minimum viable adapter.

Next: урок 04 для path C (optimization), или урок 05 если path B complete.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Какие core classes нужно implement для basic dbt adapter?

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

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

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

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