Files
fn_registry/python/functions/metabase/test_metabase_mbql_validate.py
egutierrez 4300f1242d feat(metabase): expansion de funciones Python — documents, collections, permissions, validation
Añade un conjunto amplio de funciones al paquete python/functions/metabase:
- Nuevos modulos: collections.py, documents.py, maintenance.py, permissions.py, validation.py (+ test).
- Ampliacion de cards.py, dashboards.py, client.py e __init__.py para exponer las nuevas operaciones.
- Funciones de documentos (create/get/update/delete/archive/copy/move + comentarios), grupos y memberships, permission/collection graphs, copy/move de cards y dashboards, validacion de MBQL/SQL y payloads, actualizacion segura de dashboards y fix_null_ratio.
- .md por funcion con frontmatter para que fn index los registre.
- Actualiza pyproject.toml y uv.lock con las dependencias resultantes.

Impacto: ampliamente mas cobertura de la API de Metabase desde el registry, reutilizable por apps y analisis. No toca Go ni frontend.
2026-04-13 23:31:42 +02:00

276 lines
9.9 KiB
Python

"""Tests para metabase_mbql_validate."""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from metabase.metabase_mbql_validate import metabase_mbql_validate
# ---------------------------------------------------------------------------
# DQ valido: estructura simplificada basada en card 5705 post-fix
# ---------------------------------------------------------------------------
VALID_DQ = {
"lib/type": "mbql/query",
"database": 6,
"stages": [
{
"lib/type": "mbql.stage/mbql",
"source-card": 100,
"aggregation": [
[
"sum",
{"lib/uuid": "agg-uuid-1"},
[
"field",
{"base-type": "type/Decimal", "lib/uuid": "field-uuid-1"},
"Cantidad",
],
]
],
"expressions": [
[
"-",
{
"lib/uuid": "expr-uuid-1",
"lib/expression-name": "Diferencia_Cantidad",
},
[
"field",
{"base-type": "type/Decimal", "lib/uuid": "field-uuid-2"},
"Valor_A",
],
[
"field",
{"base-type": "type/Decimal", "lib/uuid": "field-uuid-3"},
"Valor_B",
],
]
],
},
{
"lib/type": "mbql.stage/mbql",
"expressions": [
[
"case",
{
"lib/uuid": "case-uuid-1",
"lib/expression-name": "Valor medio de venta",
},
[
[
[
"=",
{"lib/uuid": "cond-uuid-1"},
[
"field",
{
"base-type": "type/Integer",
"lib/uuid": "field-uuid-4",
},
"Tickets",
],
0,
],
0,
],
[
[
"!=",
{"lib/uuid": "cond-uuid-2"},
[
"field",
{
"base-type": "type/Integer",
"lib/uuid": "field-uuid-5",
},
"Tickets",
],
0,
],
[
"/",
{"lib/uuid": "div-uuid-1"},
[
"field",
{
"base-type": "type/Decimal",
"lib/uuid": "field-uuid-6",
},
"Valor_vendido",
],
[
"field",
{
"base-type": "type/Integer",
"lib/uuid": "field-uuid-7",
},
"Tickets",
],
],
],
],
]
],
},
],
}
def test_valid_dq_returns_no_errors():
"""DQ valido retorna lista vacia."""
errors = metabase_mbql_validate(VALID_DQ)
assert errors == [], f"Esperaba 0 errores, got: {errors}"
# ---------------------------------------------------------------------------
# Check 1: UUID duplicado
# ---------------------------------------------------------------------------
def test_duplicate_uuid_detected():
"""UUID repetido en el arbol MBQL genera error."""
dq = {
"stages": [
{
"lib/type": "mbql.stage/mbql",
"expressions": [
[
"-",
{"lib/uuid": "dup-uuid-001", "lib/expression-name": "ExprA"},
["field", {"base-type": "type/Decimal", "lib/uuid": "dup-uuid-001"}, "CampoX"],
["field", {"base-type": "type/Decimal", "lib/uuid": "unique-uuid-002"}, "CampoY"],
]
],
}
]
}
errors = metabase_mbql_validate(dq)
assert any("dup-uuid-001" in e for e in errors), f"Esperaba error de UUID duplicado, got: {errors}"
# ---------------------------------------------------------------------------
# Check 2: Stage mixing — expressions referencian slots de aggregation
# ---------------------------------------------------------------------------
def test_stage_mixing_detected():
"""Stage con aggregations y expressions que referencian slots generados."""
dq = {
"stages": [
{
"lib/type": "mbql.stage/mbql",
"aggregation": [
["sum", {"lib/uuid": "agg1"}, ["field", {"base-type": "type/Decimal", "lib/uuid": "f1"}, "Precio"]]
],
"expressions": [
[
"*",
{"lib/uuid": "expr2", "lib/expression-name": "DobleSum"},
# field sin base-type y nombre 'sum' = slot ref de aggregation
["field", {"lib/uuid": "ref-slot"}, "sum"],
2,
]
],
}
]
}
errors = metabase_mbql_validate(dq)
assert any("mezcla" in e.lower() or "mixing" in e.lower() or "stage" in e.lower() for e in errors), \
f"Esperaba error de stage mixing, got: {errors}"
# ---------------------------------------------------------------------------
# Check 3: Slot ref rota — sum_99 inexistente
# ---------------------------------------------------------------------------
def test_broken_slot_ref_detected():
"""Expression que referencia sum_99 cuando solo hay 1 sum genera error."""
dq = {
"stages": [
{
"lib/type": "mbql.stage/mbql",
"aggregation": [
["sum", {"lib/uuid": "agg-s1"}, ["field", {"base-type": "type/Decimal", "lib/uuid": "f-s1"}, "Precio"]]
],
"expressions": [
[
"+",
{"lib/uuid": "expr-broken", "lib/expression-name": "SumRota"},
# sum_99 no existe (solo hay 1 sum = sum_0)
["field", {"lib/uuid": "ref-broken"}, "sum_99"],
1,
]
],
}
]
}
errors = metabase_mbql_validate(dq)
assert any("sum_99" in e for e in errors), f"Esperaba error de slot ref roto, got: {errors}"
# ---------------------------------------------------------------------------
# Check 4: Case con estructura invalida
# ---------------------------------------------------------------------------
def test_case_malformed_cases_not_pairs():
"""Case con casos que no son pares [cond, result] genera error."""
dq = {
"stages": [
{
"lib/type": "mbql.stage/mbql",
"expressions": [
[
"case",
{"lib/uuid": "case-bad", "lib/expression-name": "CaseMalo"},
# lista con un solo elemento (no par)
[["solo_elemento"]],
]
],
}
]
}
errors = metabase_mbql_validate(dq)
assert any("case" in e.lower() for e in errors), f"Esperaba error de case structure, got: {errors}"
# ---------------------------------------------------------------------------
# Check 5: Name collision en expressions
# ---------------------------------------------------------------------------
def test_name_collision_detected():
"""Dos expressions con mismo lib/expression-name en la misma stage generan error."""
dq = {
"stages": [
{
"lib/type": "mbql.stage/mbql",
"expressions": [
["-", {"lib/uuid": "e1", "lib/expression-name": "MiCalculo"},
["field", {"base-type": "type/Decimal", "lib/uuid": "f1"}, "A"],
["field", {"base-type": "type/Decimal", "lib/uuid": "f2"}, "B"]],
["+", {"lib/uuid": "e2", "lib/expression-name": "MiCalculo"},
["field", {"base-type": "type/Decimal", "lib/uuid": "f3"}, "C"],
1],
],
}
]
}
errors = metabase_mbql_validate(dq)
assert any("MiCalculo" in e for e in errors), f"Esperaba error de name collision, got: {errors}"
# ---------------------------------------------------------------------------
# Check: stages ausente
# ---------------------------------------------------------------------------
def test_missing_stages_returns_error():
"""dataset_query sin 'stages' devuelve error de estructura."""
errors = metabase_mbql_validate({"database": 1})
assert any("stages" in e.lower() for e in errors), f"Esperaba error de stages ausente, got: {errors}"