Aller au contenu

Architecture

Vue d'ensemble

Le labo reproduit, avec des briques open source, les quatre composants d'un environnement Snowflake :

Composant Implémentation au labo Équivalent Snowflake
Socle MinIO (brique du NUC), tables Apache Iceberg v2, catalogue REST Apache Polaris persisté sur PostgreSQL (brique du NUC), Trino, DuckDB Stockage et stages, format de table, couche de métadonnées et RBAC, virtual warehouses
Collecte et retraitement Paquet ingest/, projet dbt dbt/, Marquez pour le lineage Stages, COPY INTO, Snowpipe, tables, semantic views, lineage
cortex_lab Paquet cortex_lab/ : fonctions AI_* aux noms et contrats Cortex, routées vers LiteLLM (brique du NUC) Snowflake Cortex AI Functions
Prévision Paquet forecast/ et image forecast/Dockerfile (t0-gamma, CPU) Service conteneurisé dans Snowpark Container Services chez le client

Principe d'implantation (décision déléguée, voir decisions.md) : les briques d'infrastructure partagées du NUC sont consommées, jamais embarquées. Le fichier docker-compose.yml (projet Compose snowlab) ne contient que Polaris, Trino, Marquez, leurs jobs d'initialisation et le service de prévision à la demande. PostgreSQL (pg-central), MinIO (minio-central) et LiteLLM sont joints sur le réseau Docker externe data.

Hors périmètre (CLAUDE.md, PLAN.md) : performance (micro-partitions, caches, dimensionnement des warehouses), Marketplace réel, Native Apps, Secure Data Sharing, clonage d'un catalogue entier, fonctions IA de Trino, API DataFrame de Snowpark, API Retrocast, SNOWFLAKE.ML.FORECAST, toute donnée du client.

Déploiement

flowchart LR
  subgraph NUC["Briques du NUC (réseau Docker externe data)"]
    PG[("pg-central:5432<br/>snowlab_polaris_db<br/>snowlab_marquez_db")]
    S3[("minio-central:9000<br/>bucket snowlab<br/>landing/ et warehouse/")]
    LLM["LiteLLM<br/>alias cortex-classify,<br/>cortex-sentiment, cortex-complete"]
    TRAEFIK["Traefik<br/>entrypoint web, local-only@file"]
  end

  subgraph COMPOSE["docker-compose.yml (projet snowlab)"]
    MINIT["minio-init<br/>amazon/aws-cli:2.38.5"]
    PBOOT["polaris-bootstrap<br/>apache/polaris-admin-tool:1.8.0"]
    POLARIS["polaris<br/>apache/polaris:1.8.0<br/>127.0.0.1:18181"]
    PINIT["polaris-init<br/>python:3.12.15-slim-trixie"]
    TRINO["trino<br/>trinodb/trino:484<br/>127.0.0.1:18080"]
    MARQUEZ["marquez<br/>marquezproject/marquez:0.51.1<br/>127.0.0.1:15000"]
    FC["forecast (profil forecast)<br/>image forecast/Dockerfile"]
  end

  subgraph HOST["Poste (uv run)"]
    PY["ingest, dbt-duckdb,<br/>cortex_lab, data.generate, tests"]
  end

  MINIT --> S3
  PBOOT --> PG
  POLARIS --> PG
  POLARIS --> S3
  PINIT --> POLARIS
  TRINO --> POLARIS
  TRINO --> S3
  MARQUEZ --> PG
  FC --> POLARIS
  FC --> S3
  FC --> DUCK
  PY --> POLARIS
  PY --> S3
  PY --> LLM
  PY --> MARQUEZ
  PY --> DUCK
  TRAEFIK --> TRINO
  TRAEFIK --> POLARIS
  TRAEFIK --> MARQUEZ

Services du compose

Service Image Rôle Ports hôte Réseaux Redémarrage
minio-init amazon/aws-cli:2.38.5 Vérifie que l'utilisateur scopé atteint le bucket et crée les préfixes landing/ et warehouse/ (infra/minio/init.sh) aucun data no
polaris-bootstrap apache/polaris-admin-tool:1.8.0 Crée le realm POLARIS et les identifiants root dans PostgreSQL (rejouable) aucun data no
polaris apache/polaris:1.8.0 Catalogue REST Iceberg, état persisté dans snowlab_polaris_db 127.0.0.1:18181 vers 8181 lab, data, web unless-stopped
polaris-init python:3.12.15-slim-trixie Exécute infra/polaris/init_catalog.py : catalogue, principaux, rôles, droits, namespaces aucun lab no
trino trinodb/trino:484 Moteur SQL, catalogue analytics 127.0.0.1:18080 vers 8080 lab, data, web unless-stopped
marquez marquezproject/marquez:0.51.1 API OpenLineage (sans interface web) 127.0.0.1:15000 vers 5000 lab, data, web unless-stopped
docs construite depuis website/Dockerfile (nginx-unprivileged 1.31.7-alpine3.24) Site de documentation docs.openflake.nuc : page d'accueil et docs/ aucun web unless-stopped
forecast construite depuis forecast/Dockerfile Prévision t0-gamma à la demande (profil forecast) aucun lab, data no

Chaîne de dépendances (depends_on) : polaris-bootstrap terminé avec succès, puis polaris sain (healthcheck sur http://localhost:8182/q/health), et minio-init terminé, puis polaris-init terminé, puis trino. marquez ne dépend d'aucun service du compose. forecast attend polaris sain.

Réseaux :

  • lab : réseau interne du projet (Polaris joint par http://polaris:8181).
  • data : réseau externe des briques du NUC (pg-central, minio-central, LiteLLM).
  • web : réseau externe de Traefik. Quatre routeurs, tous sur l'entrypoint web avec le seul middleware local-only@file (avenant Traefik de PLAN.md) : openflake.nuc vers Trino, polaris.openflake.nuc vers Polaris, marquez.openflake.nuc vers Marquez, docs.openflake.nuc vers le site de documentation (avenant du 2026-10-26 (4)).

Tous les ports publiés sont liés à 127.0.0.1 ; PostgreSQL n'est pas publié (contrôlé par tests/test_repo_conventions.py).

Modules Python

Paquet Fichiers Rôle
lab/ config.py, naming.py, types.py, duckdb_connect.py, iceberg.py, maintenance.py Configuration lue dans l'environnement, nommage à trois niveaux, correspondance des types, connexion DuckDB (extension iceberg, catalogue Polaris, cortex_lab), accès PyIceberg, équivalents Time Travel, rétention et clone
ingest/ landing.py, detect.py, classify.py, schema_registry.py, raw_loader.py, mapping.py, exceptions.py, lineage.py, shared_snapshots.py Collecte : dépôt immuable, détection de schéma, classification, registre, chargement raw (rôle de COPY INTO et Snowpipe), mapping, exceptions et file de revue, lineage, snapshots shared_*
cortex_lab/ core/, duckdb/, snowpark.py, cortex.py, compat.py, dbt_plugin.py Fonctions AI_CLASSIFY, AI_SENTIMENT, AI_COMPLETE et macro GET
forecast/ inputs.py, model.py, writer.py, backtest.py, run.py, Dockerfile Prévision t0-gamma, tables forecast.forecast et forecast.backtest_metrics
data/ generate.py, README.md Générateurs synthétiques
dbt/ dbt_project.yml, profiles.example.yml, models/, snapshots/, seeds/, macros/, tests/ Projet dbt unique (standardized, semantic MetricFlow, SCD2, tests de qualité)
infra/ minio/, polaris/, trino/, marquez/ Configuration des services et jobs d'initialisation

Dépendances Python (pyproject.toml, toutes épinglées exactement, verrouillées par uv.lock) : pydantic==2.14.1, duckdb==2.0.0, pyarrow==26.0.0, openai==3.37.0, pyiceberg[pyarrow]==0.12.0, dbt-core==1.12.6, dbt-duckdb==1.11.0, dbt-trino==1.10.6, dbt-metricflow==0.15.0, openlineage-python==1.54.0, openlineage-dbt==1.54.0, pyyaml==6.0.3, jsonschema==4.26.0, numpy==2.5.4. Groupes : dev (par défaut : pytest==9.1.1, pyiceberg[sql-sqlite], httpx2==2.16.0), forecast (tfc-t0[torch]==0.6.0, torch==2.14.1 en roues CPU depuis l'index pytorch-cpu), snowflake (dbt-snowflake==1.12.4, installé par défaut en développement depuis le 2026-10-23, pour les contrôles B1 et B2). Aucune dépendance litellm : le client est le SDK openai pointé sur LITELLM_BASE_URL.

Flux de données

flowchart TD
  GEN["data.generate<br/>(fichiers synthétiques)"] --> PUT["LandingStore.put<br/>ingest/landing.py"]
  PUT --> LAND[("MinIO s3://snowlab/landing/source/date/")]
  LAND --> DET["detect.py<br/>sniff_csv, read_json_auto"]
  DET --> CLS["classify.py<br/>règles, puis AI_CLASSIFY"]
  CLS -->|non classé| EXC
  CLS --> LOAD["raw_loader.py<br/>COPY INTO / Snowpipe"]
  LOAD --> REG[("analytics.intake.schema_registry")]
  LOAD --> HIST[("analytics.intake.load_history")]
  LOAD --> EXC[("analytics.intake.exceptions<br/>analytics.intake.review_queue")]
  LOAD --> RAW[("analytics.raw.pms_daily<br/>bookings_reservations<br/>rate_shop_rates")]
  LOAD -.->|OpenLineage| MQZ["Marquez"]
  RAW --> DBT["dbt-duckdb<br/>(Polaris attaché sous l'alias analytics)"]
  DBT --> STD[("analytics.standardized : std_*, snap_*<br/>analytics.semantic : dim_*, fct_hotel_daily")]
  DBT -.->|seed mapping_rules| INTSEED[("analytics.intake.mapping_rules")]
  SNAP["shared_snapshots.py"] --> CAL[("analytics.shared_calendar.calendar_days")]
  STD --> FC["conteneur forecast<br/>t0-gamma"]
  CAL --> FC
  FC --> FOR[("analytics.forecast.forecast<br/>analytics.forecast.backtest_metrics")]
  MAP["mapping.py<br/>AI_COMPLETE"] --> SUG[("analytics.intake.mapping_suggestions")]
  SUG --> EXC
  CLS -.-> LLM["LiteLLM"]
  MAP -.-> LLM

Étapes, avec le module qui les porte :

  1. Dépôt : data.generate produit les fichiers d'une date ; LandingStore.put les écrit sous landing/<source>/<date>/<fichier> dans le bucket snowlab. Un fichier déposé n'est jamais remplacé.
  2. Détection et classification : ingest.detect.detect lit le schéma avec DuckDB ; ingest.classify.classify applique les règles sur le nom et les colonnes, et n'appelle AI_CLASSIFY que si les règles ne donnent pas un type unique. Un fichier non classé devient une exception et un élément de la file de revue.
  3. Chargement raw : ingest.raw_loader.RawLoader.load ajoute les colonnes de preuve et la clé de hash, enregistre l'empreinte de schéma, fait évoluer le schéma Iceberg, écrit la table raw par PyIceberg, rapproche les totaux et note le chargement dans intake.load_history (mémoire de 14 jours, comme Snowpipe).
  4. Retraitement : dbt (adapter dbt-duckdb par défaut) lit les tables raw via Polaris et écrit les couches standardized et semantic, ainsi que les snapshots SCD2, dans Polaris, en tables Iceberg analytics.standardized.* et analytics.semantic.* (Polaris est attaché à DuckDB sous le nom analytics, base DuckDB locale en mémoire).
  5. Données partagées : ingest.shared_snapshots.write_calendar_snapshot écrit un snapshot daté dans shared_calendar.calendar_days.
  6. Prévision : le conteneur forecast lit la couche standardized, les snapshots SCD2 et le calendrier partagé dans Polaris (DuckDB, noms analytics.<schéma>.<table>), sans volume local, et écrit forecast.forecast et forecast.backtest_metrics dans Iceberg.
  7. Lineage : ingest.lineage.LineageEmitter émet START, COMPLETE ou FAIL vers Marquez pour chaque chargement raw, y compris depuis la CLI python -m ingest.raw_loader quand MARQUEZ_URL est défini ; les exécutions dbt se tracent avec l'enveloppe dbt-ol d'openlineage-dbt.

Où vivent les tables

Schéma (namespace) Tables Stockage physique Écrivain
raw pms_daily, bookings_reservations, rate_shop_rates Iceberg (Polaris, s3://snowlab/warehouse/) PyIceberg (ingest/raw_loader.py)
intake load_history, schema_registry, exceptions, review_queue, mapping_suggestions Iceberg PyIceberg (ingest/)
intake (seed) mapping_rules Iceberg dbt seed (dbt-duckdb)
standardized std_pms_daily, std_bookings, std_rate_shop, snap_bookings_otb, snap_rate_shop Iceberg dbt (dbt-duckdb)
semantic dim_hotel, dim_comp_set, fct_hotel_daily, metricflow_time_spine, seeds hotels et comp_sets Iceberg dbt (dbt-duckdb)
shared_calendar calendar_days Iceberg PyIceberg (ingest/shared_snapshots.py)
forecast forecast, backtest_metrics Iceberg PyIceberg (forecast/writer.py)

Les six namespaces Polaris (raw, standardized, semantic, intake, forecast, shared_calendar) sont créés par infra/polaris/init_catalog.py. Avec l'adapter par défaut dbt-duckdb, dbt écrit directement dans ces namespaces (voie A de l'avenant du 2026-10-23) : les tables sont lisibles en analytics.<schéma>.<table> par Trino et par DuckDB. Avec la cible trino de dbt/profiles.example.yml, les mêmes modèles s'écriraient aussi en Iceberg via Trino (cible compilée dans les tests, exécution complète non couverte : à vérifier).

Identités d'accès

Client Identité Polaris Contrôle d'accès effectif
Trino toujours le principal lab_tech (rôle lab_tech_role, fichier infra/trino/catalog/analytics.properties) Règles de fichier infra/trino/rules.json selon le nom d'utilisateur Trino déclaré (aucune authentification)
PyIceberg (lab/iceberg.py) lab_tech ou lab_reader selon LabConfig.from_env("tech" ou "reader") Privilèges Polaris du rôle de catalogue
DuckDB (lab/duckdb_connect.py, profil dbt-duckdb) lab_tech par défaut Privilèges Polaris
Conteneur forecast lab_tech Privilèges Polaris
polaris-init root (création), puis lab_tech pour les namespaces Administration

Tous les moteurs lisent et écrivent les fichiers directement sur MinIO avec les clés statiques de l'utilisateur scopé snowlab : Polaris ne délivre pas d'identifiants temporaires (catalogue stsUnavailable).

Correspondance avec Snowflake

Snowflake Labo Fichiers
Base ANALYTICS, schémas, tables Catalogue Trino analytics, catalogue et namespaces Polaris, tables Iceberg v2 ; même nom à trois niveaux, en minuscules infra/trino/catalog/analytics.properties, lab/naming.py
Stockage interne, stages Bucket snowlab, préfixe landing/<source>/<date>/ infra/minio/init.sh, ingest/landing.py
COPY INTO, Snowpipe ingest.raw_loader, déclenché à la main ; fichier déjà chargé ignoré pendant 14 jours ingest/raw_loader.py
METADATA$FILENAME, METADATA$FILE_ROW_NUMBER _source_file, _row_number ingest/__init__.py, ingest/raw_loader.py
Time Travel (AT, BEFORE) FOR VERSION AS OF, FOR TIMESTAMP AS OF (Trino), lecture par snapshot (PyIceberg) lab/maintenance.py
DATA_RETENTION_TIME_IN_DAYS expire_snapshots avec rétention par défaut d'un jour lab/maintenance.py
Zero-copy clone Branche ou tag Iceberg par table lab/maintenance.py
RBAC (rôles, privilèges) Principal, rôle de principal, rôle de catalogue, privilèges Polaris ; règles de fichier Trino par-dessus infra/polaris/init_catalog.py, infra/trino/rules.json
Virtual warehouse Trino (nœud unique), DuckDB embarqué infra/trino/config.properties
VARIANT, OBJECT, ARRAY JSON en chaîne lab/types.py
TIMESTAMP_NTZ, TIMESTAMP_TZ timestamp, timestamptz (microsecondes) ; lu par Snowflake sur une table Iceberg, timestamptz devient TIMESTAMP_LTZ (voir socle.md) lab/types.py
NUMBER(38, s) decimal(38, s) lab/types.py
GET(x, 'champ'), x:champ Macro get (DuckDB) et macro dbt get avec dispatch ; la syntaxe x:champ n'est pas utilisée cortex_lab/duckdb/macros.sql, dbt/macros/get.sql
Semantic views dbt avec MetricFlow dbt/models/semantic/semantic_models.yml
Lineage Snowsight OpenLineage vers Marquez (API seule) ingest/lineage.py, infra/marquez/marquez.yml
Marketplace, Data Sharing Schémas shared_* en lecture seule alimentés par snapshots datés ingest/shared_snapshots.py
Cortex AI Functions cortex_lab (DuckDB et Python) : AI_CLASSIFY, AI_SENTIMENT, AI_COMPLETE (forme chaîne), AI_FILTER, AI_EXTRACT (texte), AI_TRANSLATE, AI_SUMMARIZE (texte), AI_AGG, AI_SUMMARIZE_AGG, AI_EMBED et AI_SIMILARITY (texte), AI_COUNT_TOKENS, AI_REDACT, PROMPT cortex_lab/
snowflake.cortex, snowflake.snowpark.functions.ai_* cortex_lab.cortex, cortex_lab.snowpark, bascule cortex_lab.compat cortex_lab/compat.py
Snowpark Container Services, Model Registry Conteneur forecast exécuté par Docker Compose forecast/Dockerfile

Écarts de fidélité connus

Écart Détail Où
Casse des identifiants Snowflake stocke les identifiants non quotés en majuscules (ANALYTICS.RAW.PMS_DAILY), le labo en minuscules ; la règle « minuscules, jamais quotés » rend le même SQL valide des deux côtés lab/naming.py
Précision des timestamps Nanosecondes par défaut pour les tables natives de Snowflake, microsecondes en Iceberg v2 des deux côtés (relu le 2026-10-23) lab/types.py, ingest/raw_loader.py
VARIANT Pas de type variant en Iceberg v2 : valeurs imbriquées stockées en JSON compact (chaîne) ingest/raw_loader.py
GET Renvoie NULL pour une clé absente comme Snowflake, mais renvoie un VARCHAR (chaîne sans guillemets, ou texte JSON) au lieu d'un VARIANT cortex_lab/duckdb/macros.sql
Snowpipe Déclenchement manuel par commande, pas d'auto-ingest par notification d'événements ingest/raw_loader.py
Clone Par table seulement (branche ou tag), pas de clone de schéma ni de base lab/maintenance.py
RBAC Trino Trino n'authentifie pas : le nom d'utilisateur déclaré choisit les règles ; Trino parle à Polaris sous l'identité lab_tech infra/trino/rules.json
Couche semantic MetricFlow au lieu de CREATE SEMANTIC VIEW dbt/models/semantic/
Emplacement de standardized et semantic (corrigé le 2026-10-23) Avant : fichier DuckDB local, Polaris attaché sous l'alias lake. Maintenant : tables Iceberg dans Polaris, alias analytics, mêmes noms à trois niveaux dans dbt, Trino, DuckDB et Snowflake. Particularités de dbt-duckdb sur Polaris : transactions désactivées (Polaris refuse le commit multi-tables), suppression sans CASCADE, nom intermédiaire unique par exécution ; une matérialisation table recrée la table à chaque exécution, donc son historique de snapshots Iceberg repart de zéro (comme CREATE OR REPLACE TABLE) dbt/profiles.example.yml, dbt/macros/
Lineage de la CLI du chargeur (corrigé le 2026-10-23) Avant : python -m ingest.raw_loader n'émettait aucun événement et le dataset de sortie était toujours raw.<table>. Maintenant : la CLI émet vers Marquez quand MARQUEZ_URL est défini, avec le schéma réel (--raw-schema) ingest/raw_loader.py, ingest/lineage.py
AI_COMPLETE Forme chaîne seulement ; la macro DuckDB renvoie toujours un VARCHAR ; guardrails à TRUE est une erreur cortex_lab/core/functions/ai_complete.py
snowflake.cortex.sentiment Renvoie un float entre -1 et 1 (forme du module Python Snowflake), pas l'objet d'AI_SENTIMENT cortex_lab/cortex.py
Limites de jetons Estimation à 4 caractères par jeton, pas de tokenizer du modèle géré (AI_COUNT_TOKENS comprise) cortex_lab/core/limits.py
Fenêtre de contexte Celle du modèle local (32 768 jetons en chat, 2 048 en embedding, mesurées le 2026-10-23) : au-delà, erreur par ligne, jamais une troncature silencieuse cortex_lab/core/limits.py, cortex_lab/core/client.py
Cible dbt-trino Les deux dbt build passent (le second remplace les tables) grâce à la purge de suppression activée sur le catalogue Polaris analytics ; la macro trino__list_relations_without_caching contourne le refus du catalogue system infra/polaris/init_catalog.py, dbt/macros/trino_list_relations.sql, tests/ingest/test_dbt_trino_integration.py
Lineage dbt Les datasets de dbt-ol portent l'espace de noms duckdb://:memory:, ceux du chargeur iceberg://analytics : pas de lien dans Marquez tests/ingest/test_dbt_lineage_integration.py
Cible dbt-snowflake Profil déclaré ; dbt parse et dbt compile --no-introspect aboutissent hors connexion (2026-10-23, B1 et B2) ; SQL non validé sur Snowflake, exécution chez le client dbt/profiles.example.yml, tests/ingest/test_dbt_snowflake_offline.py
Marquez API seule, pas d'interface web infra/marquez/marquez.yml

Références Snowflake

Les pages ci-dessous sont celles citées dans PLAN.md, avec leur statut de vérification à cette date.

Élément Page Vérifié
Tables Iceberg https://docs.snowflake.com/en/user-guide/tables-iceberg 2026-10-22
Types de données Iceberg https://docs.snowflake.com/en/user-guide/tables-iceberg-data-types 2026-10-23
Stages internes https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage 2026-10-22
METADATA$FILENAME, METADATA$FILE_ROW_NUMBER https://docs.snowflake.com/en/user-guide/querying-metadata 2026-10-22
Snowpipe https://docs.snowflake.com/en/user-guide/data-load-snowpipe-intro 2026-10-22
Time Travel https://docs.snowflake.com/en/user-guide/data-time-travel 2026-10-22
Zero-copy clone https://docs.snowflake.com/en/user-guide/tables-storage-considerations 2026-10-22
CREATE ... CLONE https://docs.snowflake.com/en/sql-reference/sql/create-clone 2026-10-23
Contrôle d'accès https://docs.snowflake.com/en/user-guide/security-access-control-overview 2026-10-22
Identifiants https://docs.snowflake.com/en/sql-reference/identifiers-syntax 2026-10-22
Types date et heure https://docs.snowflake.com/en/sql-reference/data-types-datetime 2026-10-22
Types semi-structurés https://docs.snowflake.com/en/sql-reference/data-types-semistructured 2026-10-22
GET https://docs.snowflake.com/en/sql-reference/functions/get 2026-10-22
Semantic views https://docs.snowflake.com/en/user-guide/views-semantic/overview 2026-10-22
CREATE SEMANTIC VIEW https://docs.snowflake.com/en/sql-reference/sql/create-semantic-view 2026-10-22
Lineage https://docs.snowflake.com/en/user-guide/ui-snowsight-lineage 2026-10-23
Data Sharing https://docs.snowflake.com/en/user-guide/data-sharing-intro 2026-10-23
Snowpark Container Services https://docs.snowflake.com/en/developer-guide/snowpark-container-services/overview 2026-10-22
Modèle custom du Model Registry https://docs.snowflake.com/en/developer-guide/snowflake-ml/model-registry/bring-your-own-model-types 2026-10-22

Les références des fonctions Cortex sont dans cortex-lab.md.