Перейти к основному содержимому

Практическое знакомство со Snowflake Horizon и Apache Iceberg

dbt v2
Snowflake
Iceberg
DuckDB
Продвинутый
Menu

    В этом руководстве вы узнаете, как выполнять преобразования dbt в двух движках (Snowflake и DuckDB), работая с единым общим набором таблиц Apache Iceberg. Вы создадите слои исходных и подготовленных данных в виде таблиц Iceberg под управлением Snowflake, а затем используете открытый REST-каталог Iceberg в Snowflake Horizon, чтобы DuckDB мог напрямую читать и записывать те же самые таблицы. В результате получится рабочий процесс с несколькими движками, в котором каждое преобразование выполняется на наиболее подходящем движке, данные никогда не копируются и не синхронизируются между системами, а управление данными и отслеживание их происхождения сохраняются на всех этапах.

    Доступ к одним и тем же таблицам Iceberg из Snowflake и DuckDB​

    К концу этого руководства ваш проект fusion-jaffle-shop будет преобразовывать данные в двух разных движках, используя единый общий набор таблиц:

    • Snowflake: Создаёт слои исходных и подготовленных данных в виде таблиц Apache Iceberg под управлением Snowflake, зарегистрированных во встроенном каталоге Horizon.
    • DuckDB: Читает те же самые таблицы Iceberg через открытый REST-каталог Iceberg в Horizon, создаёт нижестоящую модель (orders) и записывает результат обратно в виде таблицы Iceberg в том же каталоге.

    Самое интересное — этап с DuckDB. Поскольку Iceberg — это открытый формат таблиц, а Horizon поддерживает открытый протокол Iceberg REST, внешний движок, такой как DuckDB, может работать напрямую с таблицами Snowflake, для которых действуют правила управления данными. Оба решения читают и записывают одни и те же файлы через один и тот же каталог, без отдельной копии данных.

    Если точнее, преобразование выполняется в локальном процессе DuckDB (встроенном в dbt v2); Horizon предоставляет метаданные каталога и выдаёт краткосрочные учётные данные для доступа к хранилищу, чтобы DuckDB мог обращаться к файлам, которые всё это время находятся в вашем собственном бакете S3.

    Вычислительные кластеры Snowflake (warehouse) обладают высокой производительностью. Даже одноузловой XSMALL хорошо справляется с большинством преобразований в промышленной среде, и для основной части ваших рабочих нагрузок выполнение в Snowflake остаётся правильным выбором. Но иногда полезно обращаться к тем же таблицам с централизованным управлением откуда-то ещё: для быстрого исследовательского запроса с ноутбука, для этапа, который естественно вписывается в конвейер, уже работающий на другом движке, или для задачи, ради которой не хочется запускать warehouse. REST-каталог Iceberg в Horizon делает это возможным без дублирования данных и потери управления ими.

    Почему это важно:

    • Одна копия с централизованным управлением, множество движков: Существует единственная физическая копия данных в открытом формате Parquet с метаданными Iceberg, зарегистрированная в Horizon. Snowflake, DuckDB, Spark, Trino и другие движки могут напрямую читать и записывать её. Без синхронизации и ETL между системами.
    • Управление данными сохраняется: Таблицы остаются полноценными объектами Snowflake. Они отображаются в каталоге, учитывают предоставленные права доступа и могут иметь политики маскирования, политики доступа к строкам и сведения о происхождении данных — даже если их записал DuckDB.
    • Гибкий выбор движка под задачу: Поскольку Horizon предоставляет доступ к одним и тем же таблицам из разных сред, вы можете направлять каждое преобразование в наиболее подходящий движок, в том числе выполнять отдельный этап локально в DuckDB, без дублирования данных и перестройки архитектуры конвейера.

    Это основа кроссплатформенного dbt Mesh: один проект, один каталог, множество движков.

    Примечания к руководству​

    Прежде чем начать, учтите несколько моментов:

    • Это руководство для опытных пользователей. Предполагается, что у вас есть базовые знания dbt, Snowflake, DuckDB, связанных инструментов и способов их установки.
    • Хотя dbt отлично справляется с SQL-преобразованиями, некоторые инструменты из этого руководства предъявляют строгие требования к синтаксису. Настоятельно рекомендуем удалять комментарии из примеров кода в этом руководстве, прежде чем использовать их в рабочей среде.
    • Используемый демонстрационный проект — это расширенный вариант существующего проекта Jaffle Shop. Он позволяет показать некоторые аспекты, которые вам потребуется учитывать при внедрении таких рабочих процессов в собственных проектах. Чтобы всё прошло успешно, рекомендуем использовать проект по ссылке из этого руководства, а не другие существующие проекты Jaffle Shop.
    • Если вы используете аккаунт Snowflake, не настроенный для работы с Python (например, совершенно новый пробный аккаунт), в Python-моделях проекта dbt могут возникнуть ошибки. Удаление этих моделей на время прохождения руководства устранит ошибки (хотя некоторые предупреждения, не блокирующие работу, останутся).

    Предварительные требования​

    Для прохождения этого руководства вам потребуются определённые инструменты, аккаунты и права доступа.

    Аккаунты и доступ​

    Для выполнения необходимых шагов настройки вам потребуются:

    • Аккаунт Snowflake, в котором вы можете работать с ролью ACCOUNTADMIN. В этом руководстве всё создаётся с нуля, поэтому предполагается, что вы начинаете с нового аккаунта (подойдёт пробный).
    • Аккаунт AWS, в котором вы можете создать бакет S3, а также роль и политику IAM.
    • Локальная копия проекта fusion-jaffle-shop с CSV-файлами исходных данных в seeds/.
    • Навыки выполнения SQL в Snowsight и базовых команд в терминале.

    Инструменты: установка и проверка​

    Перед началом установите и проверьте следующее:

    ТребованиеДля чегоПроверка
    dbt v2 (v2, preview.194+)Поддержка catalogs.yml v2 и подключения каталогов в DuckDBdbt --version
    DuckDB 1.5.4+Совместимость с записью в Iceberg (ATTACH, выдача учётных данных)duckdb --version
    Аккаунт AWSРазмещение бакета S3, в котором хранятся данные и метаданные Iceberg—
    Новый аккаунт SnowflakeРазмещение Horizon и управляемых таблиц Iceberg—
    Проект fusion-jaffle-shopЛокальная копия с CSV-файлами исходных данных в seeds/ls seeds/ выводит raw_*.csv

    Заполнители, используемые в руководстве​

    Во всём руководстве заменяйте эти заполнители собственными значениями:

    • <ACCOUNT_IDENTIFIER>: Идентификатор вашего аккаунта Snowflake в формате ORG-ACCOUNT (например, ABCDEFG-HI12345). Его можно найти в Snowsight: меню аккаунта → Account → View account details, или в URL аккаунта https://<ORG>-<ACCOUNT>.snowflakecomputing.com.
    • <AWS_ACCOUNT_ID>: Ваш 12-значный номер аккаунта AWS.
    • <YOUR_USER>: Ваше имя пользователя для входа в Snowflake.
    • <REGION>: Регион AWS для вашего бакета. В этом руководстве используется us-east-2; выберите регион, ближайший к региону вашего аккаунта Snowflake.

    Это руководство предназначено только для v2. Механизм catalogs.yml, на который оно опирается, недоступен в устаревшем Python-адаптере dbt-duckdb.

    Проект​

    В этом руководстве используется существенно расширенная копия традиционного проекта Jaffle Shop. Сам проект содержит больше возможностей, чем требуется для простой настройки рабочего процесса с Iceberg, поэтому вы можете использовать его для дополнительных экспериментов и наблюдения за результатами.

    Клонируйте проект fusion-jaffle-shop с GitHub:

    git clone https://github.com/matthewshaver/jaffle-shop-multi-adapter.git

    Затем перейдите в каталог проекта:

    cd fusion-jaffle-shop

    Часть 1. Настройка аккаунта Snowflake​

    Войдите в Snowsight (веб-интерфейс Snowflake) под пользователем с правами администратора. Откройте новый SQL-лист и выполните каждый из приведённых ниже блоков.

    1.1 Создайте warehouse, базу данных и схему​

    USE ROLE ACCOUNTADMIN;

    CREATE WAREHOUSE IF NOT EXISTS COMPUTE_WH
    WAREHOUSE_SIZE = 'XSMALL'
    AUTO_SUSPEND = 60
    AUTO_RESUME = TRUE;

    CREATE DATABASE IF NOT EXISTS DBT_ICEBERG;
    CREATE SCHEMA IF NOT EXISTS DBT_ICEBERG.RAW;

    1.2 Создайте роль TRANSFORMER и предоставьте ей все необходимые права​

    Вы будете использовать одну роль, TRANSFORMER, для всех операций: как для построения в Snowflake, так и для выполнения задач в DuckDB. Поскольку TRANSFORMER будет владеть каждой создаваемой ею таблицей, она автоматически получит права на чтение и запись, и впоследствии вам не потребуются дополнительные разрешения SELECT.

    USE ROLE ACCOUNTADMIN;

    CREATE ROLE IF NOT EXISTS TRANSFORMER;

    -- Let TRANSFORMER use compute and the database objects
    GRANT USAGE ON WAREHOUSE COMPUTE_WH TO ROLE TRANSFORMER;
    GRANT USAGE ON DATABASE DBT_ICEBERG TO ROLE TRANSFORMER;
    GRANT USAGE ON SCHEMA DBT_ICEBERG.RAW TO ROLE TRANSFORMER;

    -- Let TRANSFORMER create the tables dbt will build (seeds + Iceberg models)
    GRANT CREATE TABLE ON SCHEMA DBT_ICEBERG.RAW TO ROLE TRANSFORMER;
    GRANT CREATE ICEBERG TABLE ON SCHEMA DBT_ICEBERG.RAW TO ROLE TRANSFORMER;
    GRANT CREATE VIEW ON SCHEMA DBT_ICEBERG.RAW TO ROLE TRANSFORMER;

    -- Give the role to yourself so you can use it, and make it your default
    GRANT ROLE TRANSFORMER TO USER <YOUR_USER>;
    ALTER USER <YOUR_USER> SET DEFAULT_ROLE = TRANSFORMER;

    В части 3, после создания внешнего тома, мы предоставим роли TRANSFORMER доступ к нему.

    Часть 2. Создание бакета S3 и роли IAM в AWS​

    Таблицы Iceberg под управлением Snowflake хранят файлы данных и метаданных в объектном хранилище, принадлежащем вам. Snowflake получает доступ к этому бакету, принимая созданную вами роль IAM. Это двустороннее согласование, поэтому порядок действий важен.

    2.1 Создайте бакет S3​

    В консоли AWS:

    1. Перейдите в S3 → Create bucket.
    2. Bucket name: выберите глобально уникальное имя. Удобный шаблон — iceberg-<AWS_ACCOUNT_ID>-<REGION>, например iceberg-1234567890-us-east-2.
    3. Region: выберите <REGION> (например, us-east-2).
    4. Оставьте остальные настройки по умолчанию и нажмите Create bucket.
    5. (Необязательно, но удобно для порядка) откройте бакет и создайте папку/префикс jaffle-iceberg/ для хранения всех файлов Iceberg.

    2.2 Создайте политику разрешений IAM​

    В консоли AWS:

    1. Перейдите в IAM → Policies → Create policy и откройте вкладку JSON.
    2. Вставьте следующий код, заменив имя бакета в обоих блоках Resource:
    {
    "Version": "2012-10-17",
    "Statement": [
    {
    "Effect": "Allow",
    "Action": ["s3:PutObject", "s3:GetObject", "s3:GetObjectVersion", "s3:DeleteObject", "s3:DeleteObjectVersion"],
    "Resource": "arn:aws:s3:::iceberg-<AWS_ACCOUNT_ID>-<REGION>/jaffle-iceberg/*"
    },
    {
    "Effect": "Allow",
    "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
    "Resource": "arn:aws:s3:::iceberg-<AWS_ACCOUNT_ID>-<REGION>",
    "Condition": { "StringLike": { "s3:prefix": ["jaffle-iceberg/*"] } }
    }
    ]
    }
    1. Нажмите Next, задайте имя snowflake-iceberg-policy и нажмите Create policy.

    2.3 Создайте роль IAM (с временной политикой доверия)​

    Пока нельзя заполнить окончательную политику доверия: для неё нужны значения, которые Snowflake сгенерирует в части 3. Поэтому создайте роль с временной политикой доверия, а затем исправьте её.

    В консоли AWS:

    1. Перейдите в IAM → Roles → Create role.
    2. Trusted entity type: выберите AWS account.
    3. Выберите This account (<AWS_ACCOUNT_ID>). Оставьте оба флажка, "Require external ID" и "Require MFA", снятыми. (Это временная настройка доверия; вы замените её в части 3.)
    4. Нажмите Next. На странице разрешений найдите и отметьте snowflake-iceberg-policy.
    5. Нажмите Next, задайте роли имя snowflake-iceberg-role и нажмите Create role.
    6. Откройте новую роль и скопируйте её ARN (arn:aws:iam::<AWS_ACCOUNT_ID>:role/snowflake-iceberg-role) — он понадобится на следующем этапе.

    Часть 3. Подключение Snowflake к S3 через внешний том​

    Внешний том — это защищённое подключение Snowflake к бакету S3, в котором хранятся ваши данные и метаданные Iceberg. В этой части вы создадите том, завершите настройку доверия между Snowflake и вашей ролью IAM и назначите этот том используемым по умолчанию для базы данных, чтобы каждая таблица Iceberg знала, где хранить свои файлы.

    3.1 Создайте внешний том​

    Вернитесь в Snowsight (с ролью ACCOUNTADMIN), скопируйте и вставьте следующий код (замените <PLACEHOLDERS> своими данными):

    USE ROLE ACCOUNTADMIN;

    CREATE OR REPLACE EXTERNAL VOLUME ICEBERG_EXT_VOL
    STORAGE_LOCATIONS = ((
    NAME = 'iceberg-s3-<REGION>'
    STORAGE_PROVIDER = 'S3'
    STORAGE_BASE_URL = 's3://iceberg-<AWS_ACCOUNT_ID>-<REGION>/jaffle-iceberg/'
    STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::<AWS_ACCOUNT_ID>:role/snowflake-iceberg-role'
    ))
    ALLOW_WRITES = TRUE;

    3.2 Получите идентификационные данные Snowflake и завершите настройку доверия IAM​

    DESCRIBE EXTERNAL VOLUME ICEBERG_EXT_VOL;

    В выводе найдите строку STORAGE_LOCATION_1 и извлеките из её JSON два значения:

    • STORAGE_AWS_IAM_USER_ARN (выглядит как arn:aws:iam::123456789012:user/abc1-s)
    • STORAGE_AWS_EXTERNAL_ID (выглядит как ABC12345_SFCRole=...=)

    Теперь вернитесь в консоль AWS: IAM → Roles → snowflake-iceberg-role → Trust relationships → Edit trust policy, удалите текущее содержимое и вставьте следующий код (со своими двумя значениями):

    {
    "Version": "2012-10-17",
    "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "<STORAGE_AWS_IAM_USER_ARN>" },
    "Action": "sts:AssumeRole",
    "Condition": { "StringEquals": { "sts:ExternalId": "<STORAGE_AWS_EXTERNAL_ID>" } }
    }]
    }

    Нажмите Update policy. Скопируйте внешний идентификатор в точности, включая все завершающие = и символы / внутри строки. Регистр имеет значение.

    3.3 Предоставьте доступ к тому и назначьте его используемым по умолчанию для базы данных​

    USE ROLE ACCOUNTADMIN;

    -- TRANSFORMER needs USAGE on the volume (required for credential vending)
    GRANT USAGE ON EXTERNAL VOLUME ICEBERG_EXT_VOL TO ROLE TRANSFORMER;

    -- Make this volume the default for Iceberg tables in the database.
    -- REQUIRED: when DuckDB creates a table through the REST catalog it can't
    -- name a volume, so Snowflake falls back to this default.
    ALTER DATABASE DBT_ICEBERG SET EXTERNAL_VOLUME = ICEBERG_EXT_VOL;
    ALTER DATABASE DBT_ICEBERG SET CATALOG = 'SNOWFLAKE';

    Часть 4. Создание токена программного доступа для DuckDB​

    DuckDB проходит аутентификацию в REST-каталоге Horizon по OAuth2, используя токен программного доступа (Programmatic Access Token, PAT) в качестве учётных данных. Чтобы добавить PAT в Snowsight, выполните:

    USE ROLE ACCOUNTADMIN;

    ALTER USER <YOUR_USER> ADD PROGRAMMATIC ACCESS TOKEN duckdb_pat
    ROLE_RESTRICTION = 'TRANSFORMER'
    DAYS_TO_EXPIRY = 90;

    Таблица результатов содержит два столбца. Скопируйте длинное значение token_secret (не token_name). Оно отображается только один раз; сохраните его в безопасном месте для части 6.

    Проверьте, что токен работает: Прежде чем подключать его к dbt, выполните в терминале обмен OAuth2 (именно это DuckDB делает внутри). Обратите внимание на пустое имя пользователя перед двоеточием в -u ":..." — это обязательное условие (см. FAQ):

    curl -s "https://<ACCOUNT_IDENTIFIER>.snowflakecomputing.com/polaris/api/catalog/v1/oauth/tokens" \
    -u ":<TOKEN_SECRET>" \
    --data-urlencode 'grant_type=client_credentials' \
    --data-urlencode 'scope=session:role:TRANSFORMER'

    JSON-ответ, содержащий "access_token", означает, что всё в порядке. Если вы видите Programmatic access token is invalid, скопируйте токен заново. Если в сообщении об ошибке упоминается сетевая политика, см. FAQ. Некоторые аккаунты Snowflake требуют наличия сетевой политики для создания или использования PAT.

    Часть 5. Загрузка исходных данных seed​

    В этой части вы загрузите исходные CSV-данные seed из проекта dbt в Snowflake в виде обычных таблиц. Вы выполните загрузку seed для целевой конфигурации prod, чтобы получить полный исторический набор данных, а не для dev, где заказы ограничиваются последним годом.

    5.1 Команды загрузки seed​

    Из корня проекта в dbt CLI установите пакеты и загрузите исходные таблицы в Snowflake. (Сначала убедитесь, что CSV-файлы seed присутствуют в seeds/.)

    dbt deps
    dbt seed --target prod

    5.2 Первая загрузка seed​

    Хук проекта on-run-start (insert_freshness_heartbeat()) обращается к исходной таблице, которая не существует до загрузки seed, поэтому первый запуск dbt seed может завершиться ошибкой Object 'DBT_ICEBERG.RAW.RAW_STORES' does not exist. Перед первой загрузкой seed закомментируйте блок on-run-start в dbt_project.yml:

    # on-run-start:
    # - "{{ insert_freshness_heartbeat() }}"

    Уберите комментарии после создания таблиц seed.

    При этом в DBT_ICEBERG.RAW загружаются следующие обычные таблицы Snowflake:

    • raw_customers
    • raw_orders
    • raw_items
    • raw_products
    • raw_stores
    • raw_supplies
    • raw_tweets

    Мы используем целевую конфигурацию prod — вы определите её в следующем разделе.

    Почему prod, а не dev?

    Модель stg_orders в этом проекте применяет макрос limit_in_dev, который ограничивает заказы последним годом всякий раз, когда целевая конфигурация называется dev. Данные seed исторические, поэтому построение с dev даёт пустую таблицу stg_orders (а следовательно, и пустую orders). Построение с любой целевой конфигурацией, отличной от dev, пропускает этот фильтр и возвращает полный набор данных. Дополнительные сведения см. в FAQ.

    Часть 6. Настройка проекта​

    6.1 profiles.yml​

    Создайте или отредактируйте ~/.dbt/profiles.yml. Определите три целевые конфигурации: dev и prod (обе для Snowflake) и duckdb (для выполнения задач вне Snowflake). Вставьте свой PAT в client_secret.

    При желании вы можете выполнить dbt init, чтобы создать profiles.yml в нужном месте и настроить профиль Snowflake.

    Если у вас уже есть ~/.dbt/profiles.yml, добавьте приведённый ниже профиль jaffle_shop до выполнения любых команд. При существующей конфигурации dbt init может не добавить целевую конфигурацию prod, и на последующих этапах её не удастся найти.

    jaffle_shop:
    target: prod
    outputs:
    # Everyday Snowflake target. NOTE: stg_orders is limited to the last year here.
    dev:
    type: snowflake
    account: "<ACCOUNT_IDENTIFIER>"
    user: "<YOUR_USER>"
    authenticator: externalbrowser # or your preferred auth
    role: TRANSFORMER
    database: DBT_ICEBERG
    schema: RAW
    warehouse: COMPUTE_WH
    threads: 8

    # Full-data Snowflake target used to build the Iceberg parents.
    prod:
    type: snowflake
    account: "<ACCOUNT_IDENTIFIER>"
    user: "<YOUR_USER>"
    authenticator: externalbrowser
    role: TRANSFORMER
    database: DBT_ICEBERG
    schema: RAW
    warehouse: COMPUTE_WH
    threads: 8

    # DuckDB target reads/writes Iceberg via Horizon with ZERO Snowflake compute.
    duckdb:
    type: duckdb
    path: ':memory:'
    schema: RAW
    secrets:
    - type: iceberg
    name: horizon_secret
    client_id: "" # MUST be empty (see FAQ)
    client_secret: "<TOKEN_SECRET>" # your PAT from Part 4
    oauth2_server_uri: "https://<ACCOUNT_IDENTIFIER>.snowflakecomputing.com/polaris/api/catalog/v1/oauth/tokens"
    oauth2_scope: "session:role:TRANSFORMER"

    В dbt_project.yml проекта v2 задано profile: default. Либо переименуйте ключ профиля выше в default, либо задайте profile: jaffle_shop. Эти значения должны совпадать.

    Если externalbrowser не работает

    Например, при ошибке 390190 (08004) ... SAML Identity Provider account parameter замените его аутентификацией по паролю для целевых конфигураций dev и prod, а сам пароль храните вне файла, используя переменную окружения:

    # authenticator: externalbrowser
    password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"

    6.2 catalogs.yml​

    Создайте catalogs.yml в корне проекта. Эта единственная запись описывает один и тот же каталог Horizon для обоих движков.

    catalogs:
    - name: horizon_catalog
    type: horizon
    table_format: iceberg
    config:
    snowflake:
    external_volume: ICEBERG_EXT_VOL
    duckdb:
    warehouse: DBT_ICEBERG # Horizon REST "warehouse" = the Snowflake database name
    endpoint: "https://<ACCOUNT_IDENTIFIER>.snowflakecomputing.com/polaris/api/catalog"
    secret: horizon_secret # references the profiles.yml secret above
    default_schema: RAW
    access_delegation_mode: VENDED_CREDENTIALS # Horizon vends temp S3 creds; no AWS keys needed

    6.3 dbt_project.yml​

    Добавьте флаг поведения, включающий catalogs.yml, и удалите разрешения marts на уровне проекта (они не работают в DuckDB — см. раздел 6.4 и FAQ). Добавьте ближе к началу файла:

    flags:
    use_catalogs_v2: true

    Затем в разделе models: jaffle_shop: marts: удалите блок +grants:

    # DELETE these two lines from the marts config:
    +grants:
    select: ["ACCOUNTADMIN"]

    6.4 Изменения моделей​

    Эти правки обеспечивают материализацию трёх моделей в формате Iceberg и их работу на обоих движках.

    • models/staging/stg_orders.sql: Добавьте блок конфигурации в начало файла и приведите временную метку к микросекундной точности (Iceberg не принимает наносекундную точность, используемую Snowflake по умолчанию):

      {{ config(materialized='table', catalog_name='horizon_catalog', alias='STG_ORDERS') }}

      ...а строку с временной меткой замените на:

      cast({{ dbt.date_trunc('day','ordered_at') }} as timestamp_ntz(6)) as ordered_at
    • models/marts/order_items.sql: добавьте в самое начало:

      {{ config(materialized='table', catalog_name='horizon_catalog', alias='ORDER_ITEMS') }}
    • models/marts/orders.sql: замените начальный блок конфигурации следующим:

      {{ config(
      materialized='table',
      catalog_name='horizon_catalog',
      alias='ORDERS',
      grants={} if target.name == 'duckdb' else {'select': ['ACCOUNTADMIN']},
      persist_docs={'relation': false, 'columns': false} if target.name == 'duckdb' else {'relation': true, 'columns': true}
      ) }}
    • models/marts/orders.yml: Отключите принудительное применение контракта (один контракт не может одновременно использовать имена типов Snowflake и DuckDB):

            contract:
      enforced: false
    • macros/insert_freshness_heartbeat.sql: Исключите целевую конфигурацию duckdb (heartbeat записывает данные в исходную таблицу Snowflake, которая не подключена в сеансе DuckDB):

      {% if target.name not in ('ci', 'dev', 'duckdb') %}
    • Почему alias в верхнем регистре? Snowflake хранит идентификаторы без кавычек в ВЕРХНЕМ РЕГИСТРЕ, а каталоги Iceberg чувствительны к регистру. DuckDB заключает имена моделей в нижнем регистре в кавычки, поэтому без псевдонима в верхнем регистре он будет искать "stg_orders" и не найдёт STG_ORDERS.

    Часть 7. Построение моделей​

    Здесь всё складывается воедино! Snowflake создаёт вышестоящие таблицы Iceberg для слоёв исходных и подготовленных данных, затем DuckDB читает эти же таблицы через Horizon, чтобы построить итоговую модель заказов и записать её обратно. Всё это — в одном общем каталоге.

    7.1 Этап Snowflake: создание вышестоящих таблиц Iceberg​

    dbt run --target prod -s +orders --exclude orders

    Эта команда создаёт stg_orders и order_items как таблицы Iceberg под управлением Snowflake в DBT_ICEBERG.RAW, зарегистрированные в Horizon. Поскольку вы выполняете команду с ролью TRANSFORMER, именно она владеет этими таблицами.

    7.2 Этап DuckDB: чтение и запись через Horizon​

    dbt run --target duckdb -s orders

    DuckDB подключает Horizon, читает STG_ORDERS и ORDER_ITEMS (получая временные учётные данные S3 от Horizon), строит orders локально и фиксирует результат в виде таблицы Iceberg через REST-каталог Horizon, не задействуя warehouse Snowflake.

    7.3 Проверка​

    dbt show --target duckdb --inline "select count(*) from {{ ref('orders') }}"

    Вы должны получить ненулевое количество строк (десятки тысяч). Кроме того, в Snowsight в DBT_ICEBERG.RAW появится ORDERS как таблица Iceberg, полностью записанная DuckDB.

    Подтвердите запись из другого движка: в Snowsight откройте Admin → Cost Management (или выполните запрос к snowflake.account_usage.warehouse_metering_history). Вы увидите расход кредитов на построение с prod в разделе 7.1 и отсутствие расхода на запуск DuckDB в разделе 7.2 — это подтверждает, что DuckDB читал и записывал таблицы Iceberg напрямую через REST-каталог Horizon, а не через warehouse Snowflake.

    Поздравляем!​

    Вы только что создали единый набор таблиц Apache Iceberg и преобразовали их с помощью двух разных движков! Snowflake — для слоёв исходных и подготовленных данных, DuckDB — для итоговой модели заказов, и всё это с одним общим каталогом Horizon. Поскольку Horizon поддерживает открытый протокол Iceberg REST, DuckDB смог напрямую читать и записывать эти таблицы Snowflake с централизованным управлением. Без копирования данных и привязки к поставщику, с сохранением управления данными и отслеживания их происхождения независимо от того, какой движок выполняет работу. Это основа кроссплатформенного dbt Mesh: один проект, один каталог и свобода направлять каждую задачу в наиболее подходящий движок.

    Часто задаваемые вопросы​

     Обмен токена OAuth возвращает `invalid_scope` для любой роли, даже `PUBLIC`.

    PAT необходимо отправлять с пустым client_id — как пароль HTTP Basic без имени пользователя (curl -u ":<PAT>"), что и обеспечивает client_id: "" в секрете DuckDB. Если передать реальное имя пользователя в качестве client_id, Snowflake попытается распознать его как зарегистрированный OAuth-клиент и отклонит область доступа. Решение — пустой client_id.

     Обмен возвращает `Programmatic access token is invalid`.

    Значение токена неверно — скорее всего, вы скопировали token_name вместо длинного token_secret или скопировали его не полностью. Создайте PAT заново и внимательно скопируйте token_secret.

     Создание или использование PAT завершается ошибкой, связанной с сетевой политикой.

    Некоторые аккаунты Snowflake требуют действующей сетевой политики для создания или использования PAT. Если вы столкнулись с этим, создайте сетевую политику, ограниченную диапазоном ваших внешних IP-адресов, назначьте её своему пользователю и повторите попытку. (Эта настройка намеренно не рассматривается в основной части руководства.)

     DuckDB получает `401 Unauthorized` от каталога.

    Проблема аутентификации. Проверьте: PAT действителен (используйте curl из части 4); client_id равен ""; oauth2_scope равен session:role:TRANSFORMER, и вашему пользователю действительно назначена роль TRANSFORMER; аккаунт в oauth2_server_uri/endpoint совпадает с аккаунтом, в котором был создан PAT.

     DuckDB получает `404 Not Found` при обращении к `/v1/config`.

    Неверное значение warehouse в catalogs.yml. Для Horizon это имя базы данных Snowflake (DBT_ICEBERG), а не имя записи в catalogs.yml.

     Каталог возвращает пространство имён, но не показывает таблицы, или DuckDB не может найти таблицу, которая должна быть ему доступна.

    Две распространённые причины: (1) у роли недостаточно прав для просмотра объектов, но если вы создавали таблицы с ролью TRANSFORMER (как в этом руководстве), она владеет таблицами и видит их. Если вы создавали их с другой ролью, учтите, что GRANT SELECT ON ALL TABLES не распространяется на таблицы Iceberg; необходимо выполнить GRANT SELECT ON ALL ICEBERG TABLES IN SCHEMA DBT_ICEBERG.RAW TO ROLE TRANSFORMER (а также выдать права на FUTURE ICEBERG TABLES). (2) Несовпадение регистра — см. примечание о псевдонимах в верхнем регистре ниже.

     `Catalog Error: Table with name raw.stg_orders does not exist`.

    Причина — чувствительность к регистру. Snowflake хранит STG_ORDERS; DuckDB использует "stg_orders" в кавычках. Задайте моделям Iceberg alias в верхнем регистре (alias='STG_ORDERS'). Для целого проекта лучше стандартизировать регистр идентификаторов, а не задавать псевдоним каждой модели.

     `SQL compilation error: Invalid time type scale ... TIMESTAMP_NTZ(9)`.

    Iceberg поддерживает временные метки только с микросекундной точностью. Выполните приведение к timestamp_ntz(6).

     `403 Forbidden` при чтении `.metadata.json` из S3, с `region ''` / `No credentials provided`.

    DuckDB нужны учётные данные хранилища для чтения файлов данных. Задайте access_delegation_mode: VENDED_CREDENTIALS в блоке каталога duckdb, чтобы Horizon выдавал временные учётные данные S3 для нужного региона — ключи AWS не требуются. Если ошибка возникает периодически сразу после пересоздания таблицы, просто повторите запуск; новый процесс заново получит актуальные метаданные и учётные данные.

     `403 Authorization failed` при `POST .../namespaces/RAW/tables` (создание).

    Для базы данных не задан внешний том по умолчанию, поэтому REST-операция createTable не может определить, куда записывать данные. Выполните ALTER DATABASE DBT_ICEBERG SET EXTERNAL_VOLUME = ICEBERG_EXT_VOL; (а также SET CATALOG = 'SNOWFLAKE';). Также убедитесь, что у TRANSFORMER есть право CREATE ICEBERG TABLE для схемы и USAGE для тома.

     `500 ... Object 'ORDERS' already exists as TABLE`.

    Обычная таблица (не Iceberg) с таким именем уже существует в этом пространстве имён (например, после предыдущего полного запуска dbt build). Удалите её: DROP TABLE IF EXISTS DBT_ICEBERG.RAW.ORDERS; и повторите запуск, чтобы DuckDB мог создать версию в формате Iceberg.

     `unknown method: map has no method named warn_once` при назначении прав.

    Макрос apply_grants из dbt-duckdb сталкивается с несовместимостью с v2. В любом случае назначение прав не имеет смысла в DuckDB — сделайте grant_config пустым для целевой конфигурации DuckDB (grants={} if target.name == 'duckdb' else {...}) и удалите +grants из конфигурации marts на уровне проекта, чтобы эти настройки не добавлялись обратно при объединении.

     `Not implemented Error: Only ALTER TABLE is supported for Iceberg`.

    persist_docs генерирует операторы COMMENT, которые механизм записи Iceberg в DuckDB не поддерживает. Отключите его для целевой конфигурации DuckDB (persist_docs={'relation': false, 'columns': false} if target.name == 'duckdb' else {...}).

     `Type with name number does not exist` (DuckDB предлагает `numeric`).

    Принудительно применяемый контракт модели генерирует CREATE TABLE с указанием типов Snowflake (number, timestamp_ntz), которые DuckDB не распознаёт. Один контракт не может удовлетворять требованиям двух движков, поэтому задайте contract: enforced: false для моделей, которые вы строите в DuckDB.

     `orders` успешно создаётся, но содержит 0 строк.

    stg_orders оказалась пустой, потому что вы построили её с целевой конфигурацией dev, где limit_in_dev ограничивает заказы последним годом, а данные seed старше. Постройте вышестоящие модели с prod (или любой целевой конфигурацией, отличной от dev).

     `TRANSFORMER` не может выполнить `count(*)` / `must specify a warehouse`.

    Командам работы с метаданными (show, describe) не нужен warehouse, но запросам он необходим. Убедитесь, что выполнено GRANT USAGE ON WAREHOUSE COMPUTE_WH TO ROLE TRANSFORMER. Чтобы получить количество строк без warehouse, прочитайте summary.total-records из метаданных таблицы Iceberg через REST-эндпоинт loadTable.

     Как распространить этот подход на другие модели помимо `orders`?

    Перенесите переопределения для DuckDB (пустые grants, отключённый persist_docs) с уровня отдельных моделей в конфигурацию marts, учитывающую целевую конфигурацию, в dbt_project.yml, стандартизируйте регистр идентификаторов во всём проекте и добавьте catalog_name/alias каждой модели, которую хотите материализовать в формате Iceberg. Всё, что записывает Snowflake, DuckDB может прочитать, и наоборот.

    Нашли ошибку?

    0