Skip to main content

Contribute a dbt Core v2 adapter

Adapter creation
Advanced
Menu
    info

    This guide is for dbt Core v2. For dbt Core v1, see Build, test, document, and promote adapters.

    Step 1: Introduction

    dbt Core v2 adapters work very differently from v1, and it's worth understanding why before diving in.

    In dbt Core v1, every adapter was a standalone Python package, independently maintained by the community. That model didn't scale: maintenance cost grew with every new warehouse added, bugs had to be fixed separately in each package, and shared improvements rarely made it back across the ecosystem.

    dbt Core v2 flips this entirely. Adapters now live inside a single Rust monorepo, organized by feature area rather than by warehouse. A fix in the auth module benefits every adapter at once. When you contribute a new adapter, you're extending shared code, not building a whole new package from scratch. In Rust terms: adding arms to existing match expressions. The more adapters that exist, the easier each new adapter becomes. Your contribution makes the whole ecosystem better.

    Another advantage of dbt Core v2: connection management lives in its own lane outside of the adapter. In v1, each adapter had to own its connection logic, wrapping vendor SDKs or implementing the Python DB API spec. In v2, ADBC drivers handle that responsibility: pre-compiled binaries that you register but don't write. For a full explanation of ADBC and how dbt Core v2 uses it, see ADBC in dbt Core v2.

    What you need to contribute is the exact logic that varies by warehouse: credentials, relation naming, macros, and catalog queries. A complete community adapter touches ~13 files; that's the bar we're aiming for.

    This guide walks you through the architecture, crate structure, and how to contribute a dbt Core v2 adapter step by step. I’m Hope Watson, product manager for dbt Core v2 adapters at dbt Labs, and I put this together to help design, document, and shepherd this process for the community in the v2 world.

    The Exasol adapter, contributed by Marco Nätlitz, is used as the reference example throughout. The full file breakdown in the reference section defines exactly what you're building.

    How v1 vs. v2 adapters differ

    Topicdbt Core v1dbt Core v2
    Package structureSeparate Python packages (e.g. dbt-snowflake, dbt-bigquery), each inheriting from dbt-adaptersPart of the dbt-core monorepo, contributed as PRs
    Who maintains themdbt Labs maintains base adapter and 6 major adapters; others community-maintained independentlydbt Labs maintains the monorepo; community contributes via PRs
    LanguagePythonRust (with Jinja SQL macros still used for SQL logic)
    How they loadDynamically loaded at runtime via Python's plugin systemCompiled into the monorepo binary
    Connection managementEach adapter implements its own connection logic, built on vendor Python SDK connectors/driversADBC drivers handle connection management: a unified driver interface that abstracts away wire protocol details
    Code organizationOrganized by warehouse, each adapter is its own isolated codebaseOrganized by feature area (vertical): a fix in one crate benefits all adapters
    Loading table...

    What you're building

    As a community contributor, you're building the foundation: connect your warehouse, run dbt macros, and support basic materializations. Once your adapter is in place, dbt build and dbt run should work. The ~13 files in the reference section define the complete scope.

    Think of your adapter contribution as the bridge between dbt Core v2 and your warehouse: everything dbt needs to talk to it, nothing more. The architecture diagram below shows where your work fits in the stack.

    dbt Core v2 adapter architecture diagram showing the community contribution zone within the monorepo, the ADBC C ABI boundary, the ADBC driver, and the warehousedbt Core v2 adapter architecture diagram showing the community contribution zone within the monorepo, the ADBC C ABI boundary, the ADBC driver, and the warehouse

    Step 2: Prerequisites

    Rust familiarity: You don't need to be a Rust expert. You need to read Rust, understand enum match expressions, and make sense of compiler errors. If Rust is new to you, skim The Rust Book Chapter 6 (enums and pattern matching). Those are the concepts you'll encounter most.

    dbt fundamentals: Understand how profiles.yml works, what materializations are, and what adapter dispatch macros do.

    Deep knowledge of your warehouse: As you work through this guide, you'll need to know how your warehouse behaves:

    • What character does it use for quoting identifiers?
    • Does it support two-part or three-part relation names?
    • What connection parameters does it require?
    • How does it handle transactions?
    • What datatypes map to standard SQL types like string, timestamp, boolean?
    • What's the most efficient way to list columns in a schema?

    A note on AI-assisted development: dbt Core v2's architecture is naturally legible to AI coding tools. Match arms are explicit, compiler errors are precise, and the scope of each file is narrow. You don't need to use an LLM, but if you do, the structure works in your favor.

    Your ADBC driver

    Do you have an ADBC driver for your warehouse? If no, stop here. The ADBC driver is a firm requirement before creating a dbt Core v2 adapter.

    Check these sources to find out if one exists for your warehouse:

    If no driver exists yet, building one is a separate project that comes before the adapter contribution. This is outside the scope of dbt Labs. Columnar specializes in building ADBC drivers and may be a useful resource.

    Long-term: signed driver distribution

    dbt Labs is working toward a model where community drivers can be distributed and loaded safely via signed binaries, similar to how operating systems verify installers. The goal is to make third-party driver installation as seamless and trustworthy as first-party. This isn't in place yet, but dbt Labs is considering this direction.

    Step 3: Understanding the codebase shape before you build

    The vertical model

    dbt Core v2 organizes adapter logic by capability, not by warehouse. There's no SnowflakeAdapter class, no BigQueryAdapter class. Instead, a single shared codebase handles all warehouses, with warehouse-specific behavior driven by match adapter_type() expressions. When you add a new warehouse, you're adding arms to those expressions across a set of feature areas, auth, relations, catalog introspection, Jinja macros, rather than creating a new top-level package.

    The structural diagram below shows how the layers fit together. Your work lives in the ADAPTER layer: the crates that handle warehouse identity, credential resolution, relation logic, and Jinja dispatch. XDBC (driver loading and connection pooling) sits just below and has a small registration step too.

    dbt Core v2 system layers diagram showing USER, CLI, PIPELINE, TASK RUNNER, ADAPTER (highlighted as the contribution zone), XDBC, and WAREHOUSE layersdbt Core v2 system layers diagram showing USER, CLI, PIPELINE, TASK RUNNER, ADAPTER (highlighted as the contribution zone), XDBC, and WAREHOUSE layers

    Crate map

    In Rust, a crate is a package: the unit of compilation, roughly equivalent to a "library" or "module" in other languages. The dbt-core repo is a monorepo of multiple crates, each responsible for one vertical slice of functionality across all warehouses. This is a quick-reference map of the six crates you'll touch to build your adapter, in the order you'll work through them in Step 5.

    CrateLocationWhat you touch
    dbt-adapter-corecrates/dbt-adapter-core/AdapterType enum variant, quote_char arm
    dbt-xdbccrates/dbt-xdbc/ADBC driver registration (Backend enum, library name)
    dbt-schemascrates/dbt-schemas/profiles.yml config struct (DbConfig variant)
    dbt-authcrates/dbt-auth/Credential resolution, connection URI construction
    dbt-adaptercrates/dbt-adapter/Relation quoting, metadata catalog queries, adapter match arms, column builder, sql_types
    dbt-loadercrates/dbt-loader/Jinja SQL macros (dbt_macro_assets/dbt-<adapter>/)
    Loading table...

    Where your adapter fits in the execution model

    By the time your adapter is invoked, all the dbt work is already done. dbt has resolved ref() and source() calls, rendered every Jinja template, and produced dialect-specific SQL. Your adapter receives a finished string and executes it.

    The adapter never sees un-rendered SQL

    dbt resolves ref(), renders Jinja, and hands your adapter a finished string. You execute it; you don't template it.

    While you're coding, the compiler enforces completeness. When you add AdapterType::MyWarehouse to the enum, every match adapter_type() block that doesn't handle it fails at that exact line. Your to-do list is always visible. For example:

    // You add your variant to the enum in dbt-adapter-core:
    pub enum AdapterType {
    Snowflake,
    BigQuery,
    DuckDb,
    Trino,
    MyWarehouse, // ← you added this
    }

    // Now every match block in the codebase must handle it.
    // This one in adapter_impl.rs doesn't yet:
    match self.adapter_type() {
    AdapterType::Snowflake => { ... }
    AdapterType::BigQuery => { ... }
    AdapterType::DuckDb => { ... }
    AdapterType::Trino => { ... }
    // missing MyWarehouse → compile error
    }
    error[E0004]: non-exhaustive patterns: `AdapterType::MyWarehouse` not covered
    --> crates/dbt-adapter/src/adapter/adapter_impl.rs:142:18
    |
    | match self.adapter_type() {
    | ^^^^^^^^^^^^^^^^^^^ pattern `AdapterType::MyWarehouse` not covered

    Each failure is a concrete implementation task.

    Are you porting an existing v1 adapter?

    No v1 adapter? Skip to Step 4. If you're porting, use this map to see exactly what transfers, what needs rethinking, and what dbt Labs now owns for you in v2.

    Diagram showing which v1 adapter components map directly to v2 (green), require rethinking (purple), or are absorbed by dbt Labs or the ADBC driver (red)Diagram showing which v1 adapter components map directly to v2 (green), require rethinking (purple), or are absorbed by dbt Labs or the ADBC driver (red)

    Most community contributors aren't building from scratch, they're porting an adapter they already maintain or use in Python for dbt Core v1. If that's you, you have a significant head start, and this is the most realistic path for the vast majority of contributors.

    Before writing any Rust, check a couple of things:

    1. Does a v1 adapter already exist for your warehouse?

    Check the trusted adapters and community adapters lists. If one exists, find its GitHub repo: the macro SQL and connection logic are almost directly reusable.

    1. Did dbt Labs already add a placeholder for your warehouse?

    Some warehouses already appear in v2's AdapterType enum but aren't fully implemented yet: the enum variant exists, which means the boilerplate is partially in place. When you add the remaining code, the compiler shows you exactly what's still missing. The warehouses in this state are:

    Warehousev1 adapterdbt Core v2 status
    Athenadbt-athena (trusted)AdapterType::Athena exists: needs auth, macros, adapter arms
    Trinodbt-trino (trusted)AdapterType::Trino exists: needs auth, macros, adapter arms
    Starburstdbt-trino (trusted)AdapterType::Starburst exists: needs auth, macros, adapter arms
    Dremiodbt-dremio (trusted)AdapterType::Dremio exists: needs auth, macros, adapter arms
    Oracledbt-oracle (trusted)AdapterType::Oracle exists: needs auth, macros, adapter arms
    Loading table...

    For warehouses not yet in AdapterType at all (MySQL, Hive, Vertica, SQL Server, Teradata, etc.), you start from Step 5.1 by adding the AdapterType variant.

    What transfers from v1 to v2

    v1 componentWhere it goes in v2Notes
    macros/adapters.sqldbt-loader/.../dbt-<wh>/macros/adapters.sqlMostly a direct port, same <wh>__ dispatch prefix, same macro names, same Jinja patterns
    macros/catalog.sqlSame locationThe catalog SQL (list_relations_without_caching, get_catalog) transfers almost verbatim
    Custom materializations and adapter-overridesSame locationLook out for Jinja that might not yet be supported in dbt Core v2; that will need to be addressed separately
    Profile fields in credentials.py / profile_template.ymlDbConfig struct in dbt-schemasEach profile field becomes a struct field: optional fields use Option<T>
    Connection URI / DSN construction in connections.pydbt-auth/src/<wh>/mod.rsThe URI building logic maps cleanly to the auth module pattern
    BaseRelation.quote_policy / identifier casing behaviorinclude_policy() in crates/dbt-adapter/src/relation/relation_impl.rs, Policy is a type alias for ResolvedQuoting defined in crates/dbt-schemas/src/schemas/relations/base.rsThe 3-part vs. 2-part name structure and quote flags map 1:1 to the dbt Core v2 Policy struct
    Catalog introspection SQL in macros and adapter.pyget_relation.rs and Jinja macrosThe system catalog table names and queries you already know transfer directly
    Loading table...

    What doesn't transfer by design

    Four categories of v1 code simply don't exist in v2, which means less code to port:

    • Connection management: In v1 you owned the full connection lifecycle against the Python DB API. In v2 the ADBC driver handles this entirely; skip anything related to cursors, retries, or connection pooling.
    • Query execution: Shared Rust infrastructure handles raw query plumbing (execute, add_query). You don't implement it.
    • Adapter class hierarchy: The Python subclass structure is replaced by shared Rust match expressions. You register your adapter; you don't implement execution logic.
    • Packaging and distribution: No setup.py, no PyPI release. dbt Labs ships your adapter once the PR is merged.

    Step 4: Development machine setup

    # Rust
    rustup # install from https://rustup.rs
    rustup show # verify

    # Go (needed for driver builds)
    go version # verify

    # Clone the repo
    git clone https://github.com/dbt-labs/dbt-core
    cd dbt-core

    # Verify you can build
    cargo build --bin dbt

    If you hit Z3 errors:

    brew install pkg-config z3

    If disk fills during build:

    cargo clean  # frees old build artifacts; you'll do this often

    Development workflow

    The core development loop is the same whether you write code manually or with an AI assistant:

    1. Add AdapterType::MyWarehouse to the enum (Step 5.1)
    2. Run cargo build -p <crate>: the compiler lists every match arm missing for your new variant
    3. Use the compiler output + the file you're editing + the equivalent reference file (from the breakdown at the bottom) to write each arm. If using an AI assistant, paste all three in as context.
      • Example: to fill in the quote_char arm, paste the error listing AdapterType::MyWarehouse as missing, the quote_char match block from dbt-adapter-core/src/lib.rs, and the Exasol line (Exasol => '"') as the pattern to follow.
    4. Fill in the arm; verify with cargo build again
    5. Repeat for each crate until error-free

    Every missing case is a compile error, so an AI assistant always has a precise specification to work from.

    What context helps for each arm:

    • The file you're editing: paste the relevant match block or function. For example, the match self.adapter_type() block from adapter_impl.rs that needs a new arm.
    • The equivalent reference file from the breakdown below. For example, when writing your auth module, paste crates/dbt-auth/src/exasol/mod.rs as the pattern.
    • The compiler error output. For example: error[E0004]: non-exhaustive patterns: AdapterType::MyWarehouse not covered
    • Your warehouse's specifics: system catalog table names and the connection fields from your profiles.yml.

    Watch out for:

    • Hallucinated file paths: AI often invents dbt Core v2 paths. Use the file breakdown below as ground truth.
    • Always verify with the type checker: run cargo build -p <crate> after any AI-generated changes.
    • SQL macro patterns from v1 may not apply cleanly in dbt Core v2. Compare against the reference adapters.sql at crates/dbt-loader/src/dbt_macro_assets/dbt-exasol/macros/adapters.sql in dbt-labs/dbt-core.

    Step 5: Build a new adapter

    This step walks you through each crate you need to touch. Work through them in order. Each builds on the last. After each sub-step, run the type checker to catch missed match arms and type errors:

    cargo build -p <crate-name>

    Replace <crate-name> with the crate you just edited, for example dbt-adapter-core, dbt-xdbc, dbt-schemas, dbt-auth, dbt-adapter, or dbt-loader.

    5.1: Register the adapter type

    Crate: crates/dbt-adapter-core/

    AdapterType is the central enum that identifies your warehouse throughout the entire codebase. Add your variant here first.

    Rust's match expressions must handle every possible variant explicitly. So when you add AdapterType::MyWarehouse, every match adapter_type() block that doesn't handle your variant becomes a compile error. Run cargo build -p <crate> and the compiler hands you a complete list of exactly what still needs to be written. Nothing is hidden or implicit.

    // crates/dbt-adapter-core/src/lib.rs
    pub enum AdapterType {
    Postgres, Snowflake, Bigquery, Databricks, Redshift,
    Salesforce, Spark, DuckDB, Fabric,
    ClickHouse, Athena, Starburst, Trino, Datafusion, Dremio, Oracle,
    Exasol, // ← already added
    MyWarehouse, // ← add yours here
    }

    Also add a quote_char arm in the same file. Use double quotes '"' for most warehouses; BigQuery and Databricks use a backtick instead:

    // crates/dbt-adapter-core/src/lib.rs
    fn quote_char(&self) -> char {
    match self {
    // ... existing adapters ...
    Exasol => '"', // double-quote (most warehouses)
    MyWarehouse => '"', // ← add yours here
    BigQuery | Databricks => '`',
    }
    }

    5.2: Register the ADBC driver

    Crate: crates/dbt-xdbc/

    Your driver already exists (covered in Step 2). This step is where you register it so dbt Core v2 knows its library name and how to load it at runtime.

    FileWhat to doRequired?
    src/driver.rsAdd a variant to the Backend enum; add the ADBC library name (e.g. "adbc_driver_exasol") and FFI protocol; also define a LoadStrategyYes
    src/install.rsAdd CDN download URL and platform stringsNo, only for drivers distributed via the dbt Labs CDN, which requires separate coordination with dbt Labs
    Loading table...

    The Backend enum maps to the ADBC shared library name (lib<name>.so / <name>.dll / lib<name>.dylib). You're registering its identity so dbt Core v2 knows what to load. You're not writing the driver here.

    CDN-distributed vs. manual install

    For these 11 adapters, dbt Core v2 automatically downloads the driver on first use. All other adapters require manual installation. Users take one extra setup step. Make this clear in your documentation (Step 7).

    On the CDN: auto-downloaded
    Snowflake, BigQuery, ClickHouse, Postgres, Databricks, Redshift, DuckDB, DuckDB Extended, Salesforce, Spark, SQL Server
    Loading table...

    For custom Arrow type mappings (for example, DuckDB needed this for HUGEINT, UTINYINT): only add warehouse-specific type handling if your driver returns types that Arrow's standard schema doesn't cover. Most warehouses don't need this.

    5.3: Add your connection profile

    Crate: crates/dbt-schemas/

    FileWhat to do
    src/schemas/profiles.rsAdd a DbConfig variant and a config struct with all connection fields
    Loading table...

    The config struct should include everything a user would put in their profiles.yml for your warehouse. Example from Exasol:

    #[derive(Debug, Clone, Serialize, Deserialize, Default, DbtSchema, Merge)]
    pub struct ExasolDbConfig {
    pub user: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", alias = "pass")]
    pub password: Option<String>,
    pub host: Option<String>,
    pub port: Option<StringOrInteger>,
    pub database: Option<String>,
    pub schema: Option<String>,
    pub encryption: Option<bool>,
    pub certificate_validation: Option<bool>,
    pub certificate_fingerprint: Option<String>,
    pub connection_timeout: Option<StringOrInteger>,
    pub threads: Option<StringOrInteger>,
    }

    After adding the config struct, also add DbConfig::MyWarehouse(Box<MyWarehouseDbConfig>) as a new variant to the DbConfig enum. The compiler will then point you at every place that reads from DbConfig and needs a new case for your warehouse. Follow those errors to wire it in.

    5.3.1: Add dbt init profile generation (optional but strongly encouraged)

    This is what makes your adapter easy to use. When someone runs dbt init and selects your warehouse, they get a guided prompt that builds their profiles.yml, without documentation hunting or manual YAML editing.

    Add support in crates/dbt-init/:

    1. Register your adapter in get_available_adapters() in crates/dbt-init/src/profile_setup.rs:
    pub fn get_available_adapters() -> &'static [AdapterType] {
    &[
    // ... existing adapters ...
    AdapterType::MyWarehouse, // ← add this
    ]
    }
    1. Create a config file crates/dbt-init/src/adapter_config/<warehouse>_config.rs implementing InteractiveSetup for your DbConfig struct: define the fields dbt init should prompt for and a set_field() handler. See postgres_config.rs as a minimal reference.

    2. Export it from crates/dbt-init/src/adapter_config/mod.rs:

    pub mod mywarehouse_config;
    pub use mywarehouse_config::setup_mywarehouse_profile;
    1. Add a match arm in create_profile_for_adapter() in profile_setup.rs:
    AdapterType::MyWarehouse => {
    let config = match existing_config {
    Some(DbConfig::MyWarehouse(c)) => Some(c),
    _ => None,
    };
    DbConfig::MyWarehouse(setup_mywarehouse_profile(config.map(Box::as_ref))?)
    }

    5.4: Add authentication

    Crate: crates/dbt-auth/

    FileWhat to doRequired?
    src/<warehouse>/mod.rsCredential resolution, reads config fields, env vars, key files, tokens; builds the ADBC connection URI and credentialsYes
    src/<warehouse>/init.rsInit SQL generation, SQL that must run when the connection opens (e.g. USE SCHEMA, SET statements, extension loading)Optional, only for warehouses that need SQL on connection open. Most don't.
    src/lib.rsRegister the new module with mod <warehouse>; and wire it into the auth dispatch matchYes
    Loading table...

    The auth module turns a DbConfig into a live, authenticated ADBC connection. At minimum you need basic credential handling. More sophisticated auth (OAuth, SSO, key-pair) can be added incrementally.

    The pattern is: read config fields → construct URI → call builder.with_parse_uri(uri), builder.with_username(user), builder.with_password(password).

    5.5: Build the adapter layer

    Crate: crates/dbt-adapter/

    This is the largest step. You're adding warehouse-specific behavior to the shared adapter layer via match adapter_type() arms.

    Simple vs. complex adapters

    Most adapters, including Exasol, Athena, Trino, Starburst, Dremio, Oracle, and ClickHouse, add match arms directly to the shared files in src/relation/.

    Only adapters with highly custom relation logic (Snowflake's multi-part names and case rules; BigQuery's project/dataset structure) have their own subdirectory under src/relation/.

    If your warehouse uses standard schema.table or database.schema.table naming with straightforward quoting, you're a simple adapter. Start simple and only add complexity if the compiler forces it.

    Relation type and quoting

    File: crates/dbt-adapter/src/relation/relation_impl.rs

    Add a match arm that sets your quoting policy. The policy controls three things: whether the database is included in fully-qualified names, whether the schema is quoted, and whether the identifier is quoted.

    For example, Exasol uses a 2-part name (schema.table, no database prefix) and quotes both:

    // crates/dbt-adapter/src/relation/relation_impl.rs
    fn include_policy(adapter_type: AdapterType, path: &RelationPath) -> Policy {
    match adapter_type {
    // ... existing adapters ...
    AdapterType::MyWarehouse => Policy::new(false, true, true),
    // ↑ ↑ ↑
    // database schema identifier
    // disabled quoted quoted
    _ => Policy::trues(), // default: all parts included and quoted
    }
    }

    Decide up front whether your warehouse uses 2-part or 3-part names, and whether identifiers are case-sensitive. For example, Exasol uppercases unquoted identifiers by default, so all catalog lookup SQL uses upper() comparisons.

    File: src/relation/factory.rs

    Add your AdapterType to the create_static_relation match, wiring it to RelationStatic (the generic static relation used for Jinja's api.Relation):

    Databricks | Spark | Fabric | DuckDB | Exasol | Postgres | Redshift | Salesforce | Bigquery => {
    let relation_type = RelationStatic { adapter_type, quoting };
    StaticBaseRelationObject::new(Arc::new(relation_type))
    }

    Catalog introspection

    File: src/metadata/get_relation.rs

    Add a match arm and a function that queries your warehouse's system catalog to look up a single relation by name. Follow the reference implementations closely. This area of the codebase is expected to evolve:

    AdapterType::Exasol => exasol_get_relation(
    adapter, state, ctx, conn, database, schema, identifier, token,
    ),
    fn exasol_get_relation(
    adapter: &AdapterImpl,
    state: &State,
    ctx: &QueryCtx,
    conn: &mut dyn Connection,
    database: &str,
    schema: &str,
    identifier: &str,
    token: CancellationToken,
    ) -> AdapterResult<Option<Box<dyn BaseRelation>>> {
    let q_schema = schema.to_uppercase();
    let q_ident = identifier.to_uppercase();

    let sql = format!(
    "select 'table' as \"type\" from sys.exa_all_tables \
    where table_schema = '{q_schema}' and table_name = '{q_ident}' \
    union all \
    select 'view' from sys.exa_all_views \
    where view_schema = '{q_schema}' and view_name = '{q_ident}'"
    );
    // execute, read result, return relation
    }

    Use information_schema if your warehouse supports standard SQL, or system catalog tables (sys.*, information_schema.*) as appropriate.

    Adapter match arms

    File: src/adapter/adapter_impl.rs

    After adding AdapterType::MyWarehouse, the compiler will enumerate every match self.adapter_type() block that needs a new arm. Most arms simply delegate to an existing pattern. For example:

    // Column builder: delegate to postgres-like builder
    Exasol => Ok(Self::build_postgres_like(field, type_ops)),

    // Schema column name for listing relations
    Exasol => "name",

    // DATA_TYPE column name in information schema
    AdapterType::Exasol => "DATA_TYPE", // in src/sql_types.rs

    For capabilities your adapter doesn't support yet (like valid_incremental_strategies), return unimplemented!(). That's fine for an initial community adapter contribution.

    Column builder

    File: src/column/column_builder.rs

    Add a match arm for how your warehouse's Arrow record batches map to dbt column objects. Most adapters can delegate to build_postgres_like:

    Exasol => Ok(Self::build_postgres_like(field, type_ops)),

    Only implement custom logic if your warehouse has unusual type handling.

    5.6: Write your SQL macros

    Crate: crates/dbt-loader/

    Create a new directory at src/dbt_macro_assets/dbt-<adapter_type>/.

    Registration is automatic

    The loader discovers adapter packages by scanning src/dbt_macro_assets/. You don't need to add any Rust code to register your directory. Creating it and the dbt_project.yml is sufficient.

    FileWhat to do
    dbt_macro_assets/dbt-<warehouse>/dbt_project.ymlAdapter plugin project definition: name: dbt_<warehouse>, macro-paths: ["macros"]
    macros/adapters.sqlCore adapter macros: create_schema, drop_schema, drop_relation, rename_relation, truncate_relation, create_table_as, create_view_as, list_schemas, check_schema_exists, information_schema_name, current_timestamp, get_columns_in_relation, list_relations_without_caching
    Loading table...

    v2 uses the same adapter.dispatch() pattern as v1. Your macros use the <warehouse>__ prefix to override defaults:

    {% macro exasol__create_table_as(temporary, relation, sql) -%}
    create or replace table {{ relation }} as (
    {{ sql }}
    )
    {%- endmacro %}

    {% macro exasol__create_view_as(relation, sql) -%}
    create or replace view {{ relation }} as (
    {{ sql }}
    )
    {%- endmacro %}

    {% macro exasol__drop_relation(relation) -%}
    {% call statement('drop_relation', auto_begin=False) -%}
    drop {{ relation.type }} if exists {{ relation }} cascade
    {%- endcall %}
    {% endmacro %}

    {% macro exasol__rename_relation(from_relation, to_relation) -%}
    {% call statement('rename_relation') -%}
    rename {{ from_relation.type }} {{ from_relation }} to {{ to_relation.identifier }}
    {%- endcall %}
    {% endmacro %}

    Note: rename_relation uses only to_relation.identifier, not the full relation. Exasol's RENAME syntax doesn't take a fully qualified target.

    For catalog introspection, use your warehouse's system catalog. For example, Exasol queries sys.* instead of information_schema:

    {% macro exasol__list_relations_without_caching(schema_relation) -%}
    {% call statement('list_relations_without_caching', fetch_result=True) -%}
    select
    '{{ schema_relation.database }}' as "database",
    table_name as "name",
    table_schema as "schema",
    'table' as "type"
    from sys.exa_all_tables
    where upper(table_schema) = upper('{{ schema_relation.schema }}')
    union all
    select
    '{{ schema_relation.database }}' as "database",
    view_name as "name",
    view_schema as "schema",
    'view' as "type"
    from sys.exa_all_views
    where upper(view_schema) = upper('{{ schema_relation.schema }}')
    {%- endcall %}
    {{ return(load_result('list_relations_without_caching').table) }}
    {%- endmacro %}

    {% macro exasol__information_schema_name(database) -%}
    sys
    {%- endmacro %}

    If your warehouse is similar to an existing one (e.g. Postgres-compatible), start by delegating to that dialect's macros and only override where behavior differs:

    {% macro mywarehouse__create_table_as(temporary, relation, sql) -%}
    {{ return(postgres__create_table_as(temporary, relation, sql)) }}
    {%- endmacro %}

    Step 6: Test your adapter

    Type check after each crate

    Run the type checker after completing work in each crate to catch missed match arms and type errors immediately:

    cargo build -p dbt-adapter-core
    cargo build -p dbt-xdbc
    cargo build -p dbt-schemas
    cargo build -p dbt-auth
    cargo build -p dbt-adapter
    cargo build -p dbt-loader

    Smoke testing with a dbt project

    Run a real dbt build against your warehouse and ensure it builds error-free. At minimum, exercise table, view, incremental, and seed materializations. A clean dbt build on jaffle-shop is one example of an acceptance bar for a community adapter.

    # Build the CLI
    cargo build --bin dbt

    # Create a test project (or clone jaffle-shop)
    ./target/debug/dbt init

    # Run against your warehouse
    ./target/debug/dbt build --project-dir <your-project>

    CI testing

    CI testing for community adapter PRs is coordinated with the dbt Labs adapters team: the test infrastructure is not publicly distributed. When your PR is ready, reach out in #adapter-ecosystem on the dbt Community Slack, tag Hope Watson (@Hope Watson (dbt Labs)), and the adapters team will work with you on warehouse validation. Note that this may take some time and coordination.

    Known gap: CI requires coordination

    Community contributors cannot run CI independently. dbt Labs' CI pipeline requires certain checks. For certain adapters, that means warehouse credentials. dbt Labs is still defining this handoff process for dbt Core v2. Expect to coordinate closely with the adapters team, for the approval process to take some time, and for some back-and-forth. If you hit friction, flag it in #adapter-ecosystem.

    Step 7: Document your adapter

    Once your adapter is merged and available in a release, document it so users can find and configure it.

    Write a setup guide

    Document the profiles.yml configuration for your warehouse: what fields are required, what's optional, and example values. Follow the format of existing adapter setup guides.

    Driver installation is critical to document

    Unlike adapters distributed via the CDN, your users won't get the driver automatically: dbt Core v2 won't download it for them. Your setup guide must explain where to get the driver binary and how to install it so dbt Core v2 can find it at runtime. Without this, users will configure a valid profile and still get a connection error. Include the exact library name dbt Core v2 looks for (e.g. libadbc_driver_<yourwarehouse>.dylib) and where to put it.

    General documentation guidelines

    • Assume the reader knows dbt fundamentals but is not an expert on your warehouse's inner workings.
    • Include a complete working profiles.yml example.
    • Document any warehouse-specific quirks (e.g. 2-part vs 3-part naming, identifier case sensitivity).
    • Link to the warehouse vendor's ADBC driver documentation.

    Step 8: Promote your adapter

    Your PR must be merged first

    dbt Labs reviews and merges community adapter PRs into dbt-core. Wait until the PR is merged and the adapter ships in a published release before directing users to it.

    Community channels

    Join the dbt Community Slack and find:

    • #adapter-ecosystem: the main channel for adapter developers
    • #db-<yourwarehouse>: if a channel exists for your warehouse, let users know v2 support is available. Note: v1 adapter users will still be on the Python-based adapter and will need to migrate.

    Before you announce

    Align with the adapters team on: which materializations you're targeting in the initial implementation, any known gaps in your ADBC driver, and timeline. This prevents surprises during review and sets accurate expectations for users.

    Reference: File-by-file implementation guide

    A community-contributed dbt Core v2 adapter touches roughly 13 files, all in the public dbt-labs/dbt-core repo. The "Exasol example" column shows what it looks like in practice. Substitute your warehouse name and system catalog throughout.

    Generic pathWhat it doesExasol example
    .changes/unreleased/Features-*.yamlChangelog entrySame for all adapters
    crates/dbt-adapter-core/src/lib.rsAdd AdapterType::<Warehouse> variant and quote_char armAdapterType::Exasol
    crates/dbt-xdbc/src/driver.rsAdd Backend::<Warehouse> variant: the ADBC library name and FFI protocolBackend::Exasol, library name "adbc_driver_exasol"
    crates/dbt-schemas/src/schemas/profiles.rs<Warehouse>DbConfig struct + DbConfig::<Warehouse> variant, wired into all config match armsExasolDbConfig struct + DbConfig::Exasol variant
    crates/dbt-auth/src/<warehouse>/mod.rsAuth module, reads config fields, resolves credentials, constructs connection URIsrc/exasol/mod.rs, builds URI from host, port, user/pass, TLS options
    crates/dbt-adapter/src/adapter/adapter_impl.rsAll exhaustive match adapter_type() arms, most delegate to existing patterns; use unimplemented!() for features not yet supportedExasol adds ~10 arms, most are 1–2 lines
    crates/dbt-adapter/src/column/column_builder.rsColumn builder match arm, maps Arrow record batches to dbt column objectsExasol => Ok(Self::build_postgres_like(...))
    crates/dbt-adapter/src/metadata/get_relation.rsCatalog lookup function, queries system tables to find a relation by nameexasol_get_relation(), queries sys.exa_all_tables / sys.exa_all_views
    crates/dbt-adapter/src/relation/relation_impl.rsAdd your adapter to the include_policy() match, controls whether the database prefix is included and whether parts are quotedPolicy::new(false, true, true), database disabled, schema + identifier quoted. Default Policy::trues() includes and quotes all three parts.
    crates/dbt-adapter/src/relation/factory.rsAdd warehouse to RelationStatic arm in create_static_relationExasol added alongside Postgres, Redshift, DuckDB, etc.
    crates/dbt-adapter/src/sql_types.rsMetadata column name arms, DATA_TYPE and schema listing column namesExasol uses standard DATA_TYPE column name
    crates/dbt-loader/src/dbt_macro_assets/dbt-<warehouse>/dbt_project.ymlMacro plugin project definitiondbt-exasol/dbt_project.yml
    crates/dbt-loader/src/dbt_macro_assets/dbt-<warehouse>/macros/adapters.sqlAll required adapter macrosdbt-exasol/macros/adapters.sql, uses sys.* instead of information_schema
    Loading table...

    Reference: Useful commands

    # Type check a specific crate
    cargo build -p <crate-name>

    # Build the CLI binary
    cargo build --bin dbt

    # Run a full dbt build against your warehouse
    ./target/debug/dbt build --project-dir <your-project>

    # Free disk space
    cargo clean

    Was this page helpful?

    This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

    0
    Loading