Learning Platform
Глоссарий Troubleshooting
Урок 10.04 · 22 мин
Продвинутый
AdapterCapabilitiesBehaviorFlagFeature flags

AdapterCapability и BehaviorFlag

Не каждый warehouse поддерживает каждую dbt feature. AdapterCapability позволяет adapter’у декларировать что он поддерживает — dbt-core тогда graceful опускает или адаптирует поведение.

BehaviorFlag — более новый mechanism (dbt-adapters 1.5+) для opt-in / opt-out новых behaviors. Allows gradual rollout features без breaking existing adapters.

В этом уроке — оба mechanism с examples.


Conditional materialization: table в prod, view в dev (dbt II)

AdapterCapability

# dbt-adapters/dbt/adapters/capability.py
from enum import Enum

class Capability(str, Enum):
    SchemaMetadataByRelations = 'SchemaMetadataByRelations'
    TableLastModifiedMetadata = 'TableLastModifiedMetadata'
    TableLastModifiedMetadataBatch = 'TableLastModifiedMetadataBatch'
    # ... другие

Capability — enum значений, идентифицирующих специфические features.

Support levels:

from enum import Enum

class Support(str, Enum):
    Unknown = 'Unknown'           # default — unspecified
    Unsupported = 'Unsupported'   # adapter explicitly не supports
    NotImplemented = 'NotImplemented'   # might be added later
    Full = 'Full'                  # fully supported
    Versioned = 'Versioned'         # supported в specific warehouse versions

Declaration в adapter:

from dbt.adapters.capability import (
    Capability, CapabilityDict, CapabilitySupport, Support
)

class MyAdapter(SQLAdapter):
    @classmethod
    def capabilities(cls) -> CapabilityDict:
        return CapabilityDict({
            Capability.SchemaMetadataByRelations: CapabilitySupport(support=Support.Full),
            Capability.TableLastModifiedMetadata: CapabilitySupport(support=Support.NotImplemented),
            Capability.TableLastModifiedMetadataBatch: CapabilitySupport(support=Support.Unsupported),
        })

dbt-core checks capabilities перед использованием feature:

# В dbt-core internals
if adapter.has_feature(Capability.TableLastModifiedMetadata):
    # Use the feature
    last_modified = adapter.get_table_last_modified(relation)
else:
    # Skip / use fallback
    last_modified = None

Common capabilities

SchemaMetadataByRelations

Что это: ability fetch metadata for multiple relations в одном query (vs one-at-a-time).

Why useful: для dbt docs generate на больших projects — batch fetch metadata.

Adapters that support:

  • Snowflake: Full (DESCRIBE TABLES IN SCHEMA)
  • BigQuery: Full
  • Postgres: Full (information_schema queries)

Adapters that don’t:

  • Спецификal proprietary warehouses

Implementation:

class MyAdapter(SQLAdapter):
    @classmethod
    def capabilities(cls) -> CapabilityDict:
        return CapabilityDict({
            Capability.SchemaMetadataByRelations: CapabilitySupport(support=Support.Full),
        })
    
    def get_relations_in_schema(self, schema_relation):
        # Fetch all relations metadata in one query
        ...

TableLastModifiedMetadata

Что это: query когда relation was last modified.

Why useful: source freshness checks.

Adapters that support:

  • Snowflake: information_schema.tables.LAST_ALTERED
  • BigQuery: TABLES.last_modified_time
  • Postgres: pg_stat_user_tables.last_analyze

Implementation:

@classmethod
def capabilities(cls):
    return CapabilityDict({
        Capability.TableLastModifiedMetadata: CapabilitySupport(support=Support.Full),
    })

def calculate_freshness_from_metadata(self, source):
    # Query metadata for last_modified
    ...

TableLastModifiedMetadataBatch

Что это: batch version — fetch last_modified для multiple relations в одном query.

Why useful: scale.


How dbt-core uses capabilities

# Pseudocode из dbt-core
def check_source_freshness(adapter, source):
    if adapter.has_feature(Capability.TableLastModifiedMetadata):
        # Fast path — query metadata
        last_modified = adapter.get_metadata_freshness(source)
        return compare_to_threshold(last_modified, source.freshness)
    else:
        # Slow path — execute custom query
        last_modified = adapter.calculate_freshness_with_sql(source)
        return compare_to_threshold(last_modified, source.freshness)

dbt-core auto-detects available capabilities и chooses optimal path.

Gracefully degrade: adapter не имеющий capability — feature missing, не error.


BehaviorFlag (dbt-adapters 1.5+)

BehaviorFlag — newer mechanism для behavior changes. Different from capabilities — for opt-in / opt-out new behaviors.

# dbt-adapters/dbt/adapters/behavior.py
from dbt_common.behavior_flags import Behavior


class MyAdapterBehavior(Behavior):
    flags = [
        {
            'name': 'enable_new_caching',
            'default': False,
            'description': 'Enable new caching mechanism (faster but uses more memory)',
            'docs_url': 'https://docs.getdbt.com/...',
        },
        {
            'name': 'strict_quoting',
            'default': True,
            'description': 'Strictly enforce quoting policy',
        },
    ]

Use в adapter:

class MyAdapter(SQLAdapter):
    def list_relations_without_caching(self, schema_relation):
        if self.behavior.enable_new_caching.no_warn:
            return self._list_relations_with_new_cache(schema_relation)
        else:
            return self._list_relations_default(schema_relation)

Use в profiles.yml (or dbt_project.yml):

flags:
  enable_new_caching: True

User opts-in to new behavior.


When to use BehaviorFlag

Use case 1 — Breaking change rollout:

You want to change behavior, but не break existing users. Solution: feature flag.

# Old default behavior
def get_columns(self, relation):
    return self._old_implementation(relation)

# Add flag
def get_columns(self, relation):
    if self.behavior.use_new_columns_method.no_warn:
        return self._new_implementation(relation)
    else:
        return self._old_implementation(relation)

Initially default: False. Users opt-in.

After period — change default к True. Users who didn’t opt-in get warning.

Eventually — remove flag, only new behavior remains.

Gradual migration без big-bang breaking.

Use case 2 — Performance optimization:

New behavior faster but riskier. Some workloads not ready.

flags = [
    {
        'name': 'batch_metadata_fetch',
        'default': False,
        'description': 'Batch metadata queries (10x faster but uses more memory)',
    },
]

Performance-conscious users opt-in. Conservative users stay на default.

Use case 3 — Experimental features:

New feature not yet stable. Hide behind flag while developing.

flags = [
    {
        'name': 'enable_experimental_iceberg',
        'default': False,
        'description': 'Enable experimental Iceberg materialization (alpha)',
    },
]

Brave users test. Most stay safe.


BehaviorFlag vs AdapterCapability

AspectAdapterCapabilityBehaviorFlag
PurposeDeclare what adapter supportsOpt-in/opt-out behavior
Set byAdapter (constant)User (profile/project config)
GranularityWhole-adapterPer-feature
When to useStatic feature supportRolling out changes
DefaultAdapter决定Adapter-defined, user override

AdapterCapability = static — adapter says ‘I support X’.

BehaviorFlag = dynamic — user says ‘I want behavior Y’.


Examples в популярных adapters

dbt-postgres

class PostgresAdapter(SQLAdapter):
    @classmethod
    def capabilities(cls):
        return CapabilityDict({
            Capability.SchemaMetadataByRelations: CapabilitySupport(support=Support.Full),
            Capability.TableLastModifiedMetadata: CapabilitySupport(support=Support.Full),
        })

Postgres supports both — full.

dbt-snowflake

class SnowflakeAdapter(SQLAdapter):
    @classmethod
    def capabilities(cls):
        return CapabilityDict({
            Capability.SchemaMetadataByRelations: CapabilitySupport(support=Support.Full),
            Capability.TableLastModifiedMetadata: CapabilitySupport(support=Support.Full),
            Capability.TableLastModifiedMetadataBatch: CapabilitySupport(support=Support.Full),
        })

Snowflake supports batch metadata fetching — full.

dbt-bigquery

class BigQueryAdapter(BaseAdapter):
    @classmethod
    def capabilities(cls):
        return CapabilityDict({
            Capability.TableLastModifiedMetadata: CapabilitySupport(support=Support.Full),
        })

Different feature set due to BigQuery’s nature (no traditional information_schema).

dbt-spark

class SparkAdapter(BaseAdapter):
    @classmethod
    def capabilities(cls):
        return CapabilityDict({
            # Limited capabilities — Spark different
            Capability.SchemaMetadataByRelations: CapabilitySupport(support=Support.Unsupported),
        })

Spark uses DataFrame API differently — many SQL-features don’t apply.


Adapter version evolution

Sometimes adapter adds new capability в new version:

class MyAdapter(SQLAdapter):
    @classmethod
    def capabilities(cls):
        # Version 1.0
        if cls.adapter_version < '1.5':
            return CapabilityDict({
                Capability.TableLastModifiedMetadata: CapabilitySupport(support=Support.NotImplemented),
            })
        # Version 1.5+
        return CapabilityDict({
            Capability.TableLastModifiedMetadata: CapabilitySupport(support=Support.Full),
        })

Versioned capabilities — older versions admit don’t support, newer do.


BehaviorFlag in practice

# dbt-myadapter/dbt/adapters/myadapter/impl.py
from dbt_common.behavior_flags import Behavior


class MyAdapterBehavior(Behavior):
    flags = [
        {
            'name': 'use_iceberg_default',
            'default': False,
            'description': (
                'Use Iceberg as default materialization для tables. '
                'Otherwise uses native table format. '
                'Requires Iceberg catalog setup.'
            ),
            'docs_url': 'https://docs.getdbt.com/docs/myadapter/iceberg',
        },
    ]


class MyAdapter(SQLAdapter):
    BEHAVIORS = MyAdapterBehavior
    
    def default_materialization(self):
        if self.behavior.use_iceberg_default.no_warn:
            return 'iceberg'
        return 'table'

User opts in:

# dbt_project.yml
flags:
  use_iceberg_default: True

Or per-project:

models:
  +use_iceberg_default: True

After opt-in — все tables materialized as Iceberg.


Use BehaviorFlag для backwards compat

class MyAdapterBehavior(Behavior):
    flags = [
        {
            'name': 'enable_new_quote_policy',
            'default': False,
            'description': 'Use new quoting policy that handles edge cases better',
        },
    ]


class MyAdapter(SQLAdapter):
    def get_relation_class(self):
        if self.behavior.enable_new_quote_policy.no_warn:
            return MyNewQuotingRelation
        return MyLegacyRelation

Phase 1 (release X.0):

  • Add flag, default: False
  • Old behavior maintained для everyone
  • Document new option

Phase 2 (release X.5):

  • Change default к True
  • Warning if user explicitly sets к False (‘deprecated’)

Phase 3 (release X+1.0):

  • Remove flag
  • Only new behavior

Gradual migration. Users have time to adapt.


Попробуй сам

  1. В Python REPL:
from dbt.adapters.capability import Capability, CapabilitySupport, Support, CapabilityDict

caps = CapabilityDict({
    Capability.SchemaMetadataByRelations: CapabilitySupport(support=Support.Full),
    Capability.TableLastModifiedMetadata: CapabilitySupport(support=Support.NotImplemented),
})

print(caps.get(Capability.SchemaMetadataByRelations))
# CapabilitySupport(support=Support.Full)

print(caps.get(Capability.TableLastModifiedMetadata))
# CapabilitySupport(support=Support.NotImplemented)
  1. В custom adapter define capabilities:
class MyAdapter(SQLAdapter):
    @classmethod
    def capabilities(cls):
        return CapabilityDict({
            Capability.SchemaMetadataByRelations: CapabilitySupport(support=Support.Full),
        })
  1. Test через dbt:
dbt --debug debug
# Look for capability negotiation logs
  1. For BehaviorFlag — добавьте flag в profiles.yml:
flags:
  my_custom_flag: True

Run dbt. Verify behavior changes.


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

  1. AdapterCapability — declarative: adapter says ‘I support X’. dbt-core uses to choose code paths.

  2. Common capabilities: SchemaMetadataByRelations, TableLastModifiedMetadata, TableLastModifiedMetadataBatch.

  3. Support levels: Unknown, Unsupported, NotImplemented, Full, Versioned.

  4. BehaviorFlag (dbt-adapters 1.5+) — opt-in/opt-out behaviors. For rolling out changes без breaking existing.

  5. AdapterCapability vs BehaviorFlag:

    • Capability: static, adapter-set, declares support
    • BehaviorFlag: dynamic, user-set, opt-in changes
  6. Gradual rollout pattern: добавить flag (default False) -> change default к True -> remove flag. Years of phased migration.

  7. Production-grade adapter — use both правильно. Capabilities для feature declaration, BehaviorFlag для controlled rollout.

Проверка знанийKnowledge check
Senior пишет adapter и хочет add experimental feature без breaking existing users. AdapterCapability или BehaviorFlag?
ОтветAnswer
**BehaviorFlag** — built для этого scenario.\n\n**Why BehaviorFlag**:\n\n**AdapterCapability** говорит: 'adapter supports X' или 'adapter doesn't support X'. Static. User не может change.\n\n**BehaviorFlag** говорит: 'feature X exists, user can opt-in'. Dynamic. User control.\n\nFor experimental features — нужен opt-in mechanism.\n\n**Workflow**:\n\n**Step 1 — Define flag**:\n\n```python\n# dbt-myadapter/dbt/adapters/myadapter/impl.py\nfrom dbt_common.behavior_flags import Behavior\n\n\nclass MyAdapterBehavior(Behavior):\n flags = [\n {\n 'name': 'enable_iceberg_materialization',\n 'default': False, # off by default\n 'description': (\n 'Enable experimental Iceberg materialization. '\n 'Requires Iceberg catalog setup. '\n 'May change API in future versions.'\n ),\n 'docs_url': 'https://docs.getdbt.com/docs/myadapter/iceberg',\n },\n ]\n\n\nclass MyAdapter(SQLAdapter):\n BEHAVIORS = MyAdapterBehavior\n \n def get_materialization(self, name):\n if name == 'iceberg':\n if self.behavior.enable_iceberg_materialization.no_warn:\n return IcebergMaterialization\n else:\n raise NotImplementedError(\n 'Iceberg materialization not enabled. '\n 'Set flags.enable_iceberg_materialization: True в dbt_project.yml'\n )\n return super().get_materialization(name)\n```\n\n**Step 2 — User opts-in**:\n\nUser doesn't know feature exists by default. If they see docs, they opt-in:\n\n```yaml\n# dbt_project.yml\nflags:\n enable_iceberg_materialization: True\n\nmodels:\n marts:\n fct_orders:\n +materialized: iceberg\n```\n\nNow adapter uses experimental feature.\n\n**Step 3 — Iterate based on feedback**:\n\n- v1.0: Feature behind flag, default False\n- v1.1: Bug fixes based on user feedback\n- v1.2: API improvements\n- v1.5: Stable, default still False — give users time\n- v2.0: Default becomes True (with deprecation warning if explicitly False)\n- v2.5: Remove flag entirely (only new behavior)\n\n**Why это better than always-on**:\n\n**Always-on (without flag)**:\n\n- New behavior breaks некоторых users immediately\n- No time for testing in production\n- Bug fixes too late\n- Users force-pinned к old version\n\n**With BehaviorFlag**:\n\n- Early adopters opt-in, provide feedback\n- Bugs fixed before mainstream rollout\n- Conservative users stay safe\n- Gradual migration\n- Documentation has time to mature\n\n**Why это better than AdapterCapability**:\n\nAdapterCapability decisions static. Can't say 'opt-in to use'. Capability either there or not.\n\nBehaviorFlag — runtime knob.\n\n**Real examples**:\n\n**dbt-core**:\n\n```python\nflags = [\n {'name': 'require_explicit_package_overrides_for_builtin_materializations'},\n {'name': 'enable_iceberg'},\n {'name': 'send_anonymous_usage_stats'},\n]\n```\n\n**dbt-snowflake**:\n\n```python\nflags = [\n {'name': 'use_microbatch_with_dynamic_tables'},\n]\n```\n\nEach flag controls specific new behavior.\n\n**Documentation strategy**:\n\n```markdown\n# README\n\n## Experimental Features\n\n### Iceberg Materialization (experimental)\n\n**Status**: Experimental in v1.0. May change in future versions.\n\n**Enable**:\n\n```yaml\nflags:\n enable_iceberg_materialization: True\n```\n\n**Usage**:\n\n```yaml\nmodels:\n +materialized: iceberg\n```\n\n**Limitations**:\n- Requires Iceberg catalog setup\n- ...\n\n**Roadmap**:\n- v1.0: Behind flag, default off\n- v1.5: Default off, more stable\n- v2.0: Default on, deprecation warnings\n- v2.5: Flag removed\n```\n\nUsers know status, timeline, how to enable.\n\n**Anti-pattern — Hidden new behavior**:\n\n```python\n# WRONG: breaking change без flag\nclass MyAdapter:\n def list_relations(self, schema):\n # Was: ANSI implementation\n # Now: New batch implementation (without flag!)\n return self._new_batch_implementation(schema)\n```\n\nThis breaks users immediately. Surprise upgrades.\n\n**With flag**:\n\n```python\nclass MyAdapter:\n def list_relations(self, schema):\n if self.behavior.use_batch_metadata.no_warn:\n return self._new_batch_implementation(schema)\n return self._old_implementation(schema)\n```\n\nOpt-in. No surprises.\n\n**Production discipline**:\n\n1. **Every breaking change** — behind flag первоначально\n2. **Document well** — README, docs, blog posts\n3. **Long deprecation periods** — months to years\n4. **Communicate via release notes**\n5. **Test both old и new behaviors** в CI\n\nЭто **professional maintainership**. Users trust adapter если они not surprised by upgrades.\n\nReference: dbt's behavior_flags https://docs.getdbt.com/reference/global-configs/behavior-changes
Проверка знанийKnowledge check
dbt-core checks adapter.has_feature(Capability.X) и degrades gracefully if missing. Дайте concrete example когда это полезно.
ОтветAnswer
Graceful degradation позволяет dbt features работать на adapters с разным feature support.\n\n**Example: Source freshness checks**\n\n**Setup**:\n\n```yaml\n# schema.yml\nsources:\n - name: stripe\n tables:\n - name: orders\n loaded_at_field: created_at\n freshness:\n warn_after: { count: 12, period: hour }\n error_after: { count: 24, period: hour }\n```\n\nCommand: `dbt source freshness`.\n\n**Without TableLastModifiedMetadata capability**:\n\ndbt-core uses fallback — query the source table directly:\n\n```sql\nSELECT MAX(created_at) AS max_loaded_at FROM stripe.orders;\n```\n\nWorks on **all SQL warehouses**, but:\n- Requires reading data (potentially expensive on huge tables)\n- 'created_at' must exist as column\n- Latency for large tables\n\n**With TableLastModifiedMetadata capability**:\n\ndbt-core uses fast path — query metadata:\n\n```sql\n-- Snowflake example\nSELECT last_altered FROM information_schema.tables\nWHERE table_schema = 'stripe' AND table_name = 'orders'\n\n-- BigQuery example\nSELECT last_modified_time FROM project.dataset.INFORMATION_SCHEMA.TABLES\nWHERE table_name = 'orders'\n```\n\nBenefits:\n- Faster — metadata query is instant\n- Doesn't read data\n- Doesn't require 'loaded_at_field' column\n- Works for ANY freshness check, не just timestamp-based\n\n**dbt-core logic**:\n\n```python\n# Pseudocode\nclass SourceFreshnessRunner:\n def calculate_freshness(self, source):\n if self.adapter.has_feature(Capability.TableLastModifiedMetadata):\n # Fast path\n last_modified = self.adapter.get_relation_last_modified(source.relation)\n else:\n # Slow fallback\n sql = f'SELECT MAX({source.loaded_at_field}) FROM {source.relation}'\n result = self.adapter.execute(sql, fetch=True)\n last_modified = result.columns[0][0]\n \n # Common logic\n age = current_timestamp() - last_modified\n return self.check_thresholds(age, source.freshness)\n```\n\n**Result**:\n\n- **Snowflake** (capability: Full) -> fast metadata query\n- **BigQuery** (capability: Full) -> fast metadata query\n- **DuckDB** (capability: NotImplemented) -> fallback SQL query\n- **Adapter X** (no declaration) -> fallback (safe default)\n\nEach adapter works correctly, optimal where possible.\n\n**Another example: Schema metadata batch fetch**\n\n**Without SchemaMetadataByRelations**:\n\n```python\n# Slow — one query per relation\nfor relation in relations:\n columns = self.get_columns_in_relation(relation)\n # Build catalog entry\n\n# For 1000 relations = 1000 queries × 0.1s = 100s\n```\n\n**With SchemaMetadataByRelations**:\n\n```python\n# Fast — one query for all\nmetadata = self.adapter.get_relations_metadata_in_schema(schema)\n# Build all catalog entries from single result\n\n# 1000 relations = 1 query × 5s = 5s\n```\n\n20x speedup for `dbt docs generate`.\n\n**Example без degradation**:\n\nIf TableLastModifiedMetadataBatch unsupported, dbt-core falls back to:\n- Try batch (not available)\n- Fall back to per-relation TableLastModifiedMetadata (if supported)\n- Fall back to data query (always supported)\n\nLayered fallbacks — better behavior wherever possible, worst-case still works.\n\n**Real-world impact**:\n\nFor 'dbt source freshness' on large project:\n\n- **Snowflake (Full caps)**: 30 seconds (metadata queries)\n- **Postgres (Full caps)**: 30 seconds (similar)\n- **DuckDB (no caps)**: 5 minutes (data queries)\n- **Hypothetical no-cap adapter**: 5 minutes (fallback works)\n\nUser experience differs но **functionality preserved**.\n\n**Why это design pattern good**:\n\n1. **Forward compatibility**: new dbt features work на old adapters (with degraded performance).\n2. **Backward compatibility**: old adapters не need update for new features.\n3. **Optimal usage**: when capability available, used; когда not, fallback.\n4. **Adapter authors control**: declare what supports, control performance characteristics.\n5. **Users protected**: command works regardless of adapter's level.\n\n**Counter-example без graceful degradation**:\n\n```python\n# WRONG: hard requirement\ndef calculate_freshness(self, source):\n if not self.adapter.has_feature(Capability.TableLastModifiedMetadata):\n raise NotImplementedError('This adapter doesn't support source freshness')\n \n # ... fast path only\n```\n\nBreaks users на adapters without capability. Bad UX.\n\n**With degradation** (right):\n\n```python\ndef calculate_freshness(self, source):\n if self.adapter.has_feature(Capability.TableLastModifiedMetadata):\n # Fast path\n return self._calculate_via_metadata(source)\n # Always have fallback\n return self._calculate_via_sql(source)\n```\n\nFunctionality preserved. Performance varies.\n\n**Senior design principle**:\n\n- Features должны have fallback path\n- Capabilities — optimization hints, not requirements\n- User sees feature work everywhere\n- Adapter improvements organic\n\nЭто **graceful design** для extensible system. dbt's strength.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. AdapterCapability — что декларирует и зачем?

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

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

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

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