"""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}"