import { describe, expect, it } from 'vitest'; import { composeOverlay } from '../../../../../src/context/sl/semantic-layer.service.js'; import type { SemanticLayerSource } from '../../../../../src/context/sl/types.js'; import type { ParsedCrossModelMetric, ParsedMetricflowRelationship, ParsedSemanticModel } from '../../../../../src/context/ingest/adapters/metricflow/deep-parse.js'; import { buildMetricflowColumns, buildMetricflowJoinsForModel, buildMetricflowSemanticModelSource, countImportableMetricflowRelationships, findMatchingMetricflowTable, mapCrossModelMetricToSource, mapSemanticModelToSource, resolveMetricflowSemanticModelSourceName, rewriteMetricflowManifestJoins, toKebabCaseMetricflowName, } from '../../../../../src/context/ingest/adapters/metricflow/semantic-models.js'; const ordersModel: ParsedSemanticModel = { name: 'orders', description: 'Order facts', modelRef: 'fct_orders', dimensions: [ { name: 'status', column: 'status', type: 'string', label: 'Status', description: 'Order status' }, { name: 'ordered_at', column: 'ordered_at', type: 'time', label: 'Ordered At' }, ], measures: [ { type: 'simple', name: 'total_revenue', column: 'amount', aggregation: 'sum', label: 'Total Revenue', description: 'Revenue', filter: "status = 'completed'", }, { type: 'derived', name: 'average_revenue', expr: 'total_revenue / NULLIF(order_count, 0)', dependsOn: ['total_revenue', 'order_count'], }, ], entities: [], defaultTimeDimension: 'ordered_at', }; describe('metricflow semantic model mapping', () => { it('normalizes source names the same way the server importer did', () => { expect(toKebabCaseMetricflowName('Fct Orders!')).toBe('fct-orders'); }); it('maps a parsed semantic model to a SemanticLayerSource', () => { expect(mapSemanticModelToSource(ordersModel, 'analytics.orders')).toEqual({ name: 'fct-orders', table: 'analytics.orders', grain: ['status', 'ordered_at'], columns: [ { name: 'status', type: 'string', description: 'Order status' }, { name: 'ordered_at', type: 'time' }, ], measures: [ { name: 'total_revenue', expr: 'sum(amount)', description: 'Revenue', filter: "status = 'completed'", }, { name: 'average_revenue', expr: 'total_revenue / NULLIF(order_count, 0)', }, ], joins: [], descriptions: { dbt: 'Order facts' }, }); }); it('maps a cross-model metric to a SQL standalone source', () => { const metric: ParsedCrossModelMetric = { name: 'roas', label: 'ROAS', description: 'Return on ad spend', type: 'derived', expr: 'revenue / spend', dependsOn: [ { metricName: 'orders', alias: 'revenue' }, { metricName: 'campaigns', alias: 'spend' }, ], filter: "channel = 'paid'", }; expect(mapCrossModelMetricToSource(metric)).toEqual({ name: 'roas', sql: 'revenue / spend', descriptions: { dbt: 'Return on ad spend' }, grain: [], columns: [], measures: [ { name: 'roas', expr: 'revenue / spend', description: 'Return on ad spend', filter: "channel = 'paid'", }, ], joins: [], }); }); it('finds matching tables using target schema, exact name, dotted suffix, and underscore suffix', () => { const tables = [ { id: '1', name: 'fct_orders', catalog: null, db: 'analytics', columns: [] }, { id: '2', name: 'warehouse.marts.fct_orders', catalog: null, db: 'marts', columns: [] }, { id: '3', name: 'warehouse_fct_customers', catalog: null, db: null, columns: [] }, ]; expect(findMatchingMetricflowTable('fct_orders', tables, 'analytics')?.id).toBe('1'); expect(findMatchingMetricflowTable('fct_orders', [tables[1]], null)?.id).toBe('2'); expect(findMatchingMetricflowTable('fct_customers', [tables[2]], null)?.id).toBe('3'); expect(findMatchingMetricflowTable('missing', tables, null)).toBeUndefined(); }); it('counts only relationships whose tables and columns exist', () => { const relationships: ParsedMetricflowRelationship[] = [ { fromTable: 'orders', fromColumn: 'customer_id', toTable: 'customers', toColumn: 'id' }, { fromTable: 'orders', fromColumn: 'missing', toTable: 'customers', toColumn: 'id' }, { fromTable: 'orders', fromColumn: 'customer_id', toTable: 'missing_table', toColumn: 'id' }, ]; const tables = [ { id: '1', name: 'orders', catalog: null, db: null, columns: [{ id: 'c1', name: 'customer_id' }] }, { id: '2', name: 'customers', catalog: null, db: null, columns: [{ id: 'c2', name: 'id' }] }, ]; expect(countImportableMetricflowRelationships(relationships, tables)).toBe(1); }); it('resolves semantic-model source names to lowercase snake_case identifiers', () => { expect( resolveMetricflowSemanticModelSourceName(ordersModel, { id: '1', name: 'ANALYTICS.Fct Orders', catalog: null, db: 'analytics', columns: [], }), ).toBe('fct_orders'); expect(resolveMetricflowSemanticModelSourceName({ ...ordersModel, modelRef: 'fallback_model' }, undefined)).toBe( 'fallback_model', ); }); it('materializes entity join keys as hidden standalone columns', () => { expect( buildMetricflowColumns({ ...ordersModel, entities: [{ name: 'customer', type: 'foreign', expr: 'customer_id', description: 'FK to customers' }], }), ).toContainEqual({ name: 'customer_id', type: 'string', visibility: 'hidden', description: 'FK to customers' }); }); it('builds standalone sources with semantic-model joins', () => { const orders: ParsedSemanticModel = { ...ordersModel, modelRef: 'orders', entities: [{ name: 'customer', type: 'foreign', expr: 'customer_id' }], }; const customers: ParsedSemanticModel = { ...ordersModel, name: 'customers', modelRef: 'customers', dimensions: [{ name: 'id', column: 'id', type: 'string' }], measures: [], entities: [], }; const sourceNameByModelRef = new Map([ [orders.modelRef, 'orders'], [customers.modelRef, 'customers'], ]); const joins = buildMetricflowJoinsForModel( orders, [{ fromTable: 'orders', fromColumn: 'customer_id', toTable: 'customers', toColumn: 'id' }], sourceNameByModelRef, ); expect( buildMetricflowSemanticModelSource( { model: orders, matchedTable: undefined, sourceName: 'orders', manifestSource: null, }, joins, new Map(), ), ).toMatchObject({ name: 'orders', table: 'orders', joins: [{ to: 'customers', on: 'orders.customer_id = customers.id', relationship: 'many_to_one' }], }); }); it('builds overlays for exact manifest matches so scanned columns remain manifest-owned', () => { const manifestSource: SemanticLayerSource = { name: 'orders', table: 'analytics.orders', grain: ['id'], columns: [ { name: 'id', type: 'string' }, { name: 'customer_id', type: 'string' }, ], joins: [], measures: [], descriptions: { db: 'Orders table from scan' }, }; const overlay = buildMetricflowSemanticModelSource( { model: { ...ordersModel, modelRef: 'orders', description: 'dbt-described orders' }, matchedTable: undefined, sourceName: 'orders', manifestSource, }, [{ to: 'customers', on: 'orders.customer_id = customers.id', relationship: 'many_to_one' }], new Map(), ); expect(overlay).not.toHaveProperty('table'); expect(overlay).not.toHaveProperty('grain'); expect(overlay).not.toHaveProperty('columns'); expect(overlay).toMatchObject({ name: 'orders', joins: [{ to: 'customers', on: 'orders.customer_id = customers.id', relationship: 'many_to_one' }], descriptions: { dbt: 'dbt-described orders' }, }); const composed = composeOverlay(manifestSource, overlay); expect(composed.columns.map((column) => column.name)).toEqual(['id', 'customer_id']); expect(composed.joins).toHaveLength(1); expect(composed.descriptions).toEqual({ db: 'Orders table from scan', dbt: 'dbt-described orders' }); }); it('rewrites preserved manifest joins to synced bare source names', () => { expect( rewriteMetricflowManifestJoins( [ { to: 'analytics.customers', on: 'analytics.orders.customer_id = analytics.customers.id', relationship: 'many_to_one', }, ], new Map([ ['analytics.orders', 'orders'], ['analytics.customers', 'customers'], ]), ), ).toEqual([{ to: 'customers', on: 'orders.customer_id = customers.id', relationship: 'many_to_one' }]); }); });