---
name: metabase_mbql_from_source_card
kind: function
lang: py
domain: metabase
version: "1.0.0"
purity: impure
signature: "def metabase_mbql_from_source_card(*, database_id: int, source_card_id: int, aggregations: list[dict], joins: list[dict] | None = None, filters: list[list] | None = None, breakouts: list[dict] | None = None, expressions: list[dict] | None = None) -> dict"
description: "Construye un dataset_query MBQL que envuelve una saved card (source-card) con agregaciones, joins opcionales, filtros, breakouts y una segunda etapa de expressions calculadas. Genera automaticamente todos los lib/uuid con uuid4. Elimina la necesidad de construir el JSON MBQL manualmente para el patron comun de agregar sobre una card guardada y anadir expresiones derivadas."
tags: [metabase, mbql, dataset_query, builder, query, aggregation, join, expression, pendiente-usar]
uses_functions: []
uses_types: []
params:
  - name: database_id
    desc: "ID numerico de la database de Metabase contra la que se ejecuta la query."
  - name: source_card_id
    desc: "ID de la card guardada que actua como fuente de datos (equivalente a 'pick a saved question' en el query builder de Metabase)."
  - name: aggregations
    desc: "Lista de specs de agregacion. Cada spec es un dict con 'op' (sum|count|avg|min|max|count-distinct), 'field' (nombre de columna en la source card) y 'base_type' opcional (default type/Decimal). Para count sin campo omitir 'field'."
  - name: joins
    desc: "Lista de specs de join. Cada spec incluye: alias (str), source_card_id (int), strategy (str, default 'left-join'), fields (str, opcional), local_field (str), local_base_type (str), foreign_field_id (int), foreign_base_type (str)."
  - name: filters
    desc: "Lista de clausulas MBQL de filtro en formato raw (arrays anidados). La funcion inyecta lib/uuid a cualquier dict que no lo tenga."
  - name: breakouts
    desc: "Lista de specs de breakout. Soporta campo por nombre (field, base_type, temporal_unit) o por ID numerico (field_id, base_type, join_alias)."
  - name: expressions
    desc: "Lista de specs de expression para la segunda etapa MBQL. Cada spec tiene 'name' y 'expr' (mini-DSL recursivo). Si se proporcionan, se crea una segunda stage MBQL con esas expressions."
output: "Dict con estructura completa del dataset_query MBQL: {lib/type, database, stages}. La primera stage tiene source-card, aggregation, joins, filters y breakout. La segunda stage (solo si hay expressions) contiene las expressions calculadas. Todos los dicts internos llevan lib/uuid unicos."
returns: []
returns_optional: false
error_type: "error_py_core"
imports: []
tested: true
tests:
  - "source-card y database estan presentes en el output"
  - "todos los dicts anidados tienen lib/uuid"
  - "agregaciones producen forma correcta op+meta+field"
  - "segunda stage existe solo cuando se pasan expressions"
test_file_path: "python/functions/metabase/test_metabase_mbql_from_source_card.py"
file_path: "python/functions/metabase/metabase_mbql_from_source_card.py"
---

## Mini-DSL para expressions

El campo `expr` dentro de cada spec de expression acepta un grafo recursivo de nodos:

| Nodo | Formato | Resultado MBQL |
|---|---|---|
| Field por nombre | `{"field": "PrecioVenta", "base_type": "type/Decimal"}` | `["field", {"base-type": ..., "lib/uuid": ...}, "PrecioVenta"]` |
| Operacion | `{"op": "+", "args": [nodo1, nodo2]}` | `["+", {"lib/uuid": ...}, nodo1_mbql, nodo2_mbql]` |
| Ref a expression | `{"ref": "Margen"}` | `["expression", {"base-type": "type/Float", ...}, "Margen"]` |
| Literal | `0`, `1.5`, `"texto"` | valor directo |

Operaciones soportadas: `+`, `-`, `*`, `/`, `coalesce`, `case`, y cualquier funcion MBQL valida.

## Ejemplo (card 9918 en produccion)

```python
import sys
sys.path.insert(0, '/home/egutierrez/fn_registry/python/functions')
from metabase.metabase_mbql_from_source_card import metabase_mbql_from_source_card

q = metabase_mbql_from_source_card(
    database_id=6,
    source_card_id=5305,
    aggregations=[
        {"op": "sum", "field": "PrecioVenta", "base_type": "type/Decimal"},
        {"op": "sum", "field": "PrecioCompra", "base_type": "type/Decimal"},
    ],
    joins=[
        {
            "alias": "Centros - idCentro",
            "source_card_id": 4076,
            "fields": "none",
            "local_field": "idCentro",
            "local_base_type": "type/Text",
            "foreign_field_id": 17316,
            "foreign_base_type": "type/Text",
        },
    ],
    filters=[
        ["not-empty", {}, ["field", {"base-type": "type/Text"}, "Centros - idCentro__Companies__name"]],
    ],
    expressions=[
        {
            "name": "Margen",
            "expr": {
                "op": "coalesce",
                "args": [
                    {
                        "op": "/",
                        "args": [
                            {"op": "-", "args": [{"field": "sum"}, {"field": "sum_2"}]},
                            {"field": "sum"},
                        ],
                    },
                    0,
                ],
            },
        },
    ],
)

# Verificar shape
assert q["stages"][0]["source-card"] == 5305
assert q["database"] == 6
assert len(q["stages"]) == 2   # stage0 con agg + stage1 con expressions
```

## Uso tipico con MetabaseClient

```python
from metabase import MetabaseClient, metabase_mbql_from_source_card

client = MetabaseClient('https://metabase.example.com', 'token...')

q = metabase_mbql_from_source_card(
    database_id=6,
    source_card_id=5305,
    aggregations=[
        {"op": "sum", "field": "Ventas", "base_type": "type/Decimal"},
    ],
)

payload = {
    "name": "Ventas totales por periodo",
    "type": "question",
    "display": "table",
    "dataset_query": q,
    "visualization_settings": {},
}

card = client.request("POST", "/api/card", json=payload)
print(card["id"])
```

## Notas

- Los `lib/uuid` se generan con `uuid.uuid4()` en cada llamada, por lo que dos invocaciones producen UUIDs distintos. Esto es correcto: Metabase requiere UUIDs unicos globalmente por query, no deterministicos.
- Los filtros se pasan en formato raw para maxima flexibilidad. La funcion solo inyecta `lib/uuid` a los dicts que no lo tengan.
- La segunda stage solo se crea cuando `expressions` es no-None y no vacio. Sin expressions, el output tiene una sola stage.
- `field` en un nodo de expression refiere siempre a nombre de columna (string). Para referenciar por ID numerico usar `{"field_id": N}` (solo en breakouts por ahora; en expressions usar `{"field": "nombre"}` con el nombre del slot de aggregation como `"sum"`, `"sum_2"`, etc.).
