A dbt core-t telepíthetjük lokálisan vagy használhatnánk a Docker konténerüket. Mivel az ő kedvükért sem akarjuk root-tal futtatni a Dockert, ezért saját konténert csinálunk saját user-rel, ami egyáltalán nem nehéz. Szokásosan Postgrest hozunk be adatbázisnak, amelyet egyetlen táblával és benne egyetlen rekorddal seed-elünk.

Induláskor így néz ki a mappánk:

$ tree -aF --dirsfirst
.
├── docker/
│   ├── dbt/
│   │   └── Dockerfile
│   └── postgres/
│       └── Dockerfile
├── docker-compose.yml
├── dump.sql
├── .env
├── Makefile
└── profiles.yml

Docker

Nézzük sorjában. A .env fájlunk tartalma:

DB_PORT=5432
DB_DATABASE=dbt_test
DB_USER=root
DB_PASSWORD=root
DOCKER_USER=myuser

A docker/dbt/Dockerfile tartalma:

FROM python:3.11.4-slim-bullseye

ARG DOCKER_USER

RUN apt-get update && apt-get install -y git
RUN pip3 install dbt-postgres
RUN useradd -m -u1000 -gusers ${DOCKER_USER}

USER ${DOCKER_USER}
WORKDIR /home/${DOCKER_USER}/dbt/
ENTRYPOINT ["/usr/local/bin/dbt"]

Az itteni WORKDIR-t fogjuk bindelni Compose-ban. Gitet csak azért telepítünk a konténerben, hogy ne panaszkodjon a dbt, használni biztosan nem ezt fogjuk.

A docker/postgres/Dockerfile tartalma:

FROM postgres:12

RUN apt-get update && apt-get install -y less
ENV PAGER="/usr/bin/less -S"
CMD ["-c", "client_min_messages=warning"]

A docker-compose.yml tartalma:

version: "3"

services:

  postgres:
    build: ./docker/postgres
    restart: always
    environment:
      POSTGRES_DB: ${DB_DATABASE}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      PGDATA: /var/lib/postgresql/data
    volumes:
      - db-data:/var/lib/postgresql/data
    ports:
      - "${DB_PORT}:5432"

  dbt:
    build:
      context: ./docker/dbt
      args:
        DOCKER_USER: ${DOCKER_USER}
    restart: "no"
    profiles: ["one-off"]
    user: ${DOCKER_USER}
    environment:
      DB_PORT: ${DB_PORT}
      DB_DATABASE: ${DB_DATABASE}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - .:/home/${DOCKER_USER}/dbt/
    depends_on:
      - postgres

volumes:

  db-data:

A Makefile-ba később bekerülhetnek a dbt parancsok is. Egyelőre ennyi:

include .env

.PHONY: help
help:
  @echo "Task Runner. Usage:"
  @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf "\033[36m%-38s\033[0m %s\n", $$1, $$2}' Makefile

.PHONY: pg-shell
pg-shell: ## Open the psql shell.
  docker-compose --file docker-compose.yml \
    exec postgres psql -U ${DB_USER} -w ${DB_DATABASE}

.PHONY: seed
seed: ## Load the dump into the db.
  @PGPASSWORD=${DB_PASSWORD} psql -hlocalhost -p${DB_PORT} -U${DB_USER} -fdump.sql ${DB_DATABASE}

Mintaadatok

seed.sql:

create schema if not exists landing;
create schema if not exists staging;

drop table if exists landing.customer cascade;

create table landing.customer
(
  id         bigserial primary key,
  email      text not null,
  username   text not null,
  activated  boolean default false,
  created_at timestamp default now() not null
);

insert into landing.customer (id, email, username, activated, created_at)
  values (1, 'user1@example.com', 'user1', true, '2023-03-23 13:59:38');

Ezzel minden készen áll a teszt db elindítására:

$ docker-compose up -d
$ make seed

dbt mappák és fájlok

A db kapcsolatot a profiles.yml definiálja, pg1 a kapcsolati profil általunk választott neve. A környezeti változókat dotenvből átadtuk a Docker-konténernek. A host a Compose-beli Postgres-szolgáltatásunk neve.

pg1:
  send_anonymous_usage_stats: false
  target: dev
  outputs:
    dev:
      type: postgres
      threads: 2
      host: postgres
      port: "{{ env_var('DB_PORT') | int }}"
      dbname: "{{ env_var('DB_DATABASE') }}"
      user: "{{ env_var('DB_USER') }}"
      password: "{{ env_var('DB_PASSWORD') }}"
      schema: staging

A fentiekből úgy tűnhet, hogy itt csak a kimenetről van szó, de ez a forrás definíciója is, leszámítva a sémát (mi most landing-ből staging-be töltünk). Jusson eszünkbe, hogy kizárólag transzformációról van szó, adatbázison belül; a dbt nem tud több külső forrást kezelni, mert nem az a feladata (hanem csak a T az ELT-ből). Felvehetünk viszont dev és prod környezetet is (ez a target az outputs listában), bár a gyakorlatban nem futtatnánk kézzel prodot, szóval nem lesz rá szükség. A default target-et viszont kötelező megadni. A dbt minden egyes futtatásnál hazatelefonál, amit a send_anonymous_usage_stats kapcsol ki. Az erre való utalást csak véletlenül vettem észre a doskiban, lehetne transzparensebb az efajta működés egy szabad szoftverben (Apache-2.0).

Nekem elsőre picit zavarosnak tűnt néhány dolog, amit utólag a doski és az elnevezések inkonzisztenciáival magyaráznék. Nem tökéletes, de felfogható.

Hozzuk létre a dbt-projektünket (-s = --skip-profile-setup):

$ docker-compose run --rm dbt init -s dbt_test

Megjelenik egy dbt_test mappa default tartalommal.

$ tree -aF -L 1 --dirsfirst dbt_test/
dbt_test/
├── analyses/
├── logs/
├── macros/
├── models/
├── seeds/
├── snapshots/
├── target/
├── tests/
├── dbt_project.yml
├── .gitignore
└── README.md

Nézzük először a projektet leíró dbt_project.yml fájlt:

name: 'dbt_test'
version: '1.0.0'
config-version: 2

# This setting configures which "profile" dbt uses for this project.
profile: 'pg1'

# You probably won't need to change these!
# megj.: vajon miért nem egy paths objektum tulajdonságai ezek?
model-paths: ["models"]
analysis-paths: ["analyses"]
test-paths: ["tests"]
seed-paths: ["seeds"]
macro-paths: ["macros"]
snapshot-paths: ["snapshots"]

# Configuring models
models:
  # Be sure to namespace your model configs to your project name
  # megj.: ez vajon minek?
  dbt_test:
    # Config indicated by + and applies to all files under models/example/
    example:
      +materialized: view

Ezen belül is a models szekciót, a modell konfigurációt. Az example helyére írjuk a célsémánk nevét: staging. A materializáció table vagy view lehet (esetleg incremental, ephemeral), view az alapértelmezett, és az megfelel a staging trafóknak.

Következzen a trafók leírása a models/ mappában. Először töröljük az example mappát, majd hozzuk létre a staging-et. A források tetszőleges nevű YAML-fájlokba kerülnek, hozzuk létre a models/staging/stg__sources.yml fájlt:

version: 2

sources:
  - name: landing
    description: DW landing zone.
    loader: Airbyte
    schema: landing
    tables:
      - name: customer
        description: Original customer table.

Alapértelmezetten a schema megegyezik a name-mel, nem is kéne szerepeltetni; fel kell sorolni viszont a forrástáblákat.

Itt is elhelyezhetünk modell konfigurációt (nem kötelező), amely kerülhetne akár a fenti YAML-fájlba, vagy egy külön tetszőleges nevűbe. Hozzuk létre most a models/staging/stg__models.yml fájlt. Egyetlen forrástáblánk van, amint fentebb láttuk (landing.customer), ezért itt is egyetlen modellt látunk (staging.stg_users).

version: 2

models:
  - name: stg_users
    description: Customer table transformed.
    columns:
      - name: user_id
        description: PK
        tests:
          - unique
          - not_null
      - name: email
        description: email of the customer
        tests:
          - unique
      - name: username
        description: unique name of the customer
        tests:
          - unique
          - not_null

Maguk a modellek (más néven: a transzformációk) SQL-lekérdezések, mégpedig nem DML, hanem SELECT utasítások, ami ügyes egyszerűsítés. Íme a models/staging/stg_users.sql, amely valójában egy Jinja template:

with

-- like an import
source as (
  select * from {{ source('landing', 'customer') }}
),

-- like a function def
final as (
  select
    -- renaming
    id as user_id,
    email,
    username,
    -- categorizing/bucketing
    case when email is null then 0 else 1 end as has_email,
    -- data normalization
    case when activated = 'Y' then 1 else 0 end as is_active,
    -- type casting
    created_at::date
  from source
)

-- like a function call
select * from final

dbt futtatása

Most értünk el a dbt kipróbálásához. Először teszteljük a profilunkat:

$ docker-compose run --rm \
    dbt debug --project-dir dbt_test

Látnunk kell a környezetünk paramétereinek felsorolását, a végén egy zöld “All checks passed!” üzenettel.

Jelenlegi beállításainkkal innentől mindig szükség lesz a --project-dir dbt_test argumentumra. Átírhatnánk a dbt konténerben a WORKDIR-t, és akkor nem lenne rá szükség. Egyébként ennek a dbt parancs globális argumentumának kéne lennie, mert így kissé sután használható a dbt Docker-konténerben.

Jöhet maga a futás:

$ docker-compose run --rm \
    dbt run --project-dir dbt_test

Itt egy “Completed successfully” üzenetet kell látnunk. Mit csinál e parancs? Létrehozza a modelleket, esetünkben a fent definiált staging.stg_users view-t. Megnézhetjük egy lekérdezés eredményét a modell létrehozása nélkül is:

$ docker-compose run --rm \
    dbt show --project-dir dbt_test -m stg_users

A fenti modell konfigurációban szerepeltek mező szintű tesztek. Emellett a tests/ mappában elhelyezhetünk lekérdezéseket is, amelyek akkor sikeresek, ha nincs eredményük. Ezeket mind meghívhatjuk:

$ docker-compose run --rm \
    dbt test --project-dir dbt_test

Generálhatunk dokumentációt is egy statikus weboldal formájában a target/ mappába.

$ docker-compose run --rm \
    dbt docs generate --project-dir dbt_test