Ctrl K
ComunidadGuías…

examples/metricflow-demo/assert-translation.cjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
/**
 * Reproducible test: read MetricFlow definitions from a compiled dbt manifest
 * and assert they translate into the expected Lightdash metrics.
 *
 * Usage: node assert-translation.cjs <path/to/manifest.json>
 *
 * Uses the repo build of @lightdash/common (run `pnpm -F @lightdash/common build` first).
 */
const path = require('path');
const fs = require('fs');

const common = require(path.join(
    __dirname,
    '../../packages/common/dist/cjs/index.js',
));
const { translateMetricFlowMetrics } = common;

const manifestPath = process.argv[2];
if (!manifestPath) {
    console.error('Usage: node assert-translation.cjs <manifest.json>');
    process.exit(2);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));

const modelNamesByUniqueId = Object.fromEntries(
    Object.values(manifest.nodes)
        .filter((n) => n.resource_type === 'model')
        .map((n) => [n.unique_id, n.name]),
);

const result = translateMetricFlowMetrics({
    semanticModels: manifest.semantic_models ?? {},
    metrics: manifest.metrics ?? {},
    modelNamesByUniqueId,
});

// Expected Lightdash metrics on the `orders` model (same for both specs).
const expected = {
    // group_label carried over from config.meta
    total_revenue: { type: 'sum', sql: '${TABLE}.amount', group_label: 'Order Metrics' },
    order_count: { type: 'count', sql: '${TABLE}.order_id' },
    unique_customers: { type: 'count_distinct', sql: '${TABLE}.customer_id' },
    average_order_value: { type: 'average', sql: '${TABLE}.amount' },
    median_order_value: { type: 'median', sql: '${TABLE}.amount' },
    max_order_value: { type: 'max', sql: '${TABLE}.amount' },
    min_order_value: { type: 'min', sql: '${TABLE}.amount' },
    p95_order_value: { type: 'percentile', sql: '${TABLE}.amount', percentile: 95 },
    // Filtered simple metric → CASE WHEN over the measure
    completed_revenue: {
        type: 'sum',
        sql: "CASE WHEN (${TABLE}.status = 'completed') THEN (${TABLE}.amount) END",
    },
    // sum_boolean → sum over CASE WHEN bool THEN 1 ELSE 0
    food_order_count: {
        type: 'sum',
        sql: 'CASE WHEN (${TABLE}.is_food_order) THEN 1 ELSE 0 END',
    },
    // create_metric measure (legacy) / simple metric (latest) whose
    // config.meta hides it and groups it — hidden + group_label carry over.
    internal_order_count: {
        type: 'count_distinct',
        sql: '${TABLE}.customer_id',
        hidden: true,
        group_label: 'Internal',
    },
    // Ratio → number metric over the two input metrics
    revenue_per_order: {
        type: 'number',
        sql: '(${total_revenue} * 1.0) / NULLIF(${order_count}, 0)',
    },
    // Ratio with a filtered numerator → hidden helper metric + number metric
    completion_rate: {
        type: 'number',
        sql: '(${completion_rate_numerator} * 1.0) / NULLIF(${order_count}, 0)',
    },
    completion_rate_numerator: {
        type: 'count',
        sql: "CASE WHEN (${TABLE}.status = 'completed') THEN (${TABLE}.order_id) END",
        hidden: true,
        helper: true,
    },
    // Derived → number metric with the expression rewritten over input metrics
    revenue_per_customer: {
        type: 'number',
        sql: '${total_revenue} / ${unique_customers}',
    },
};

// Hidden helper metrics are emitted alongside a manifest metric and are not
// counted in translatedCount (unlike a plain hidden metric, which is).
const helperCount = Object.values(expected).filter((e) => e.helper).length;

// Metrics that must be skipped (unsupported in the Lightdash translation).
const expectedSkipped = [
    'cumulative_revenue', // cumulative — needs time-spine semantics
];

let failures = 0;
const fail = (msg) => {
    failures += 1;
    console.error(`  ✗ ${msg}`);
};
const ok = (msg) => console.log(`  ✓ ${msg}`);

const translated = result.metricsByModel.orders ?? {};

for (const [name, exp] of Object.entries(expected)) {
    const got = translated[name];
    if (!got) {
        fail(`expected metric "${name}" was not translated`);
        continue;
    }
    if (got.type !== exp.type) {
        fail(`metric "${name}": type ${got.type} !== ${exp.type}`);
    } else if (got.sql !== exp.sql) {
        fail(`metric "${name}": sql ${got.sql} !== ${exp.sql}`);
    } else if (exp.percentile !== undefined && got.percentile !== exp.percentile) {
        fail(`metric "${name}": percentile ${got.percentile} !== ${exp.percentile}`);
    } else if (exp.hidden !== undefined && got.hidden !== exp.hidden) {
        fail(`metric "${name}": hidden ${got.hidden} !== ${exp.hidden}`);
    } else if (exp.group_label !== undefined && got.group_label !== exp.group_label) {
        fail(`metric "${name}": group_label ${got.group_label} !== ${exp.group_label}`);
    } else {
        ok(`${name} → ${exp.type}(${exp.sql})${exp.percentile ? ` p${exp.percentile}` : ''}${exp.hidden ? ' [hidden]' : ''}${exp.group_label ? ` {${exp.group_label}}` : ''}`);
    }
}

for (const name of expectedSkipped) {
    if (translated[name]) {
        fail(`metric "${name}" should have been skipped but was translated`);
    } else if (!result.warnings.some((w) => w.includes(`"${name}"`))) {
        fail(`metric "${name}" skipped without a warning`);
    } else {
        ok(`${name} skipped with warning`);
    }
}

const expectedCount = Object.keys(expected).length - helperCount;
if (result.translatedCount !== expectedCount) {
    fail(`translatedCount ${result.translatedCount} !== ${expectedCount}`);
}

console.log(
    `\n${manifestPath}: translated=${result.translatedCount} skipped=${result.skippedCount} failures=${failures}`,
);
process.exit(failures === 0 ? 0 : 1);