import { describe, expect, it } from 'vitest'; import { buildLookmlGraph } from '../../../../../src/context/ingest/adapters/lookml/graph.js'; import type { ParsedLookmlProject } from '../../../../../src/context/ingest/adapters/lookml/parse.js'; type LooseParsedLookmlProject = Omit, 'models' | 'views'> & { models?: Array & { connectionName?: string | null }>; views?: Array & { rawSqlTableName?: string | null }>; }; const mkProject = (overrides: LooseParsedLookmlProject): ParsedLookmlProject => ({ dashboards: [], allPaths: [], ...overrides, models: (overrides.models ?? []).map((model) => ({ connectionName: null, ...model })), views: (overrides.views ?? []).map((view) => ({ rawSqlTableName: null, ...view })), }); describe('buildLookmlGraph', () => { it('assigns a single model as owner of all its included views', () => { const project = mkProject({ models: [{ path: 'orders.model.lkml', name: 'orders', includes: ['views/*.view.lkml'], explores: ['orders'] }], views: [ { path: 'views/orders.view.lkml', name: 'orders', extendsFrom: [] }, { path: 'views/customers.view.lkml', name: 'customers', extendsFrom: [] }, ], allPaths: ['orders.model.lkml', 'views/customers.view.lkml', 'views/orders.view.lkml'], }); const graph = buildLookmlGraph(project); expect(graph.ownerByViewPath.get('views/orders.view.lkml')).toBe('orders'); expect(graph.ownerByViewPath.get('views/customers.view.lkml')).toBe('orders'); expect(graph.viewsIncludedByModel.get('orders')?.sort()).toEqual([ 'views/customers.view.lkml', 'views/orders.view.lkml', ]); }); it('assigns shared views to the lexicographically-first model that includes them', () => { const project = mkProject({ models: [ { path: 'marketing.model.lkml', name: 'marketing', includes: ['views/shared.view.lkml'], explores: [] }, { path: 'orders.model.lkml', name: 'orders', includes: ['views/shared.view.lkml', 'views/orders.view.lkml'], explores: [], }, ], views: [ { path: 'views/shared.view.lkml', name: 'shared', extendsFrom: [] }, { path: 'views/orders.view.lkml', name: 'orders', extendsFrom: [] }, ], allPaths: ['marketing.model.lkml', 'orders.model.lkml', 'views/orders.view.lkml', 'views/shared.view.lkml'], }); const graph = buildLookmlGraph(project); // "marketing" sorts before "orders", so marketing owns the shared view. expect(graph.ownerByViewPath.get('views/shared.view.lkml')).toBe('marketing'); expect(graph.ownerByViewPath.get('views/orders.view.lkml')).toBe('orders'); // Both models list the shared view in their include set: expect(graph.includersByViewPath.get('views/shared.view.lkml')?.sort()).toEqual(['marketing', 'orders']); }); it('resolves transitive extends chains into dependency paths', () => { const project = mkProject({ models: [{ path: 'orders.model.lkml', name: 'orders', includes: ['views/*.view.lkml'], explores: [] }], views: [ { path: 'views/base.view.lkml', name: 'base', extendsFrom: [] }, { path: 'views/orders.view.lkml', name: 'orders', extendsFrom: ['base'] }, { path: 'views/orders_ext.view.lkml', name: 'orders_ext', extendsFrom: ['orders'] }, ], allPaths: ['orders.model.lkml', 'views/base.view.lkml', 'views/orders.view.lkml', 'views/orders_ext.view.lkml'], }); const graph = buildLookmlGraph(project); expect(graph.extendsAncestorsByViewName.get('orders_ext')?.sort()).toEqual(['base', 'orders']); expect(graph.extendsAncestorsByViewName.get('orders')?.sort()).toEqual(['base']); expect(graph.extendsAncestorsByViewName.get('base')?.sort()).toEqual([]); }); it('resolves glob-style include patterns (views/*.view.lkml) against allPaths', () => { const project = mkProject({ models: [{ path: 'orders.model.lkml', name: 'orders', includes: ['views/*.view.lkml'], explores: [] }], views: [ { path: 'views/a.view.lkml', name: 'a', extendsFrom: [] }, { path: 'views/sub/b.view.lkml', name: 'b', extendsFrom: [] }, ], allPaths: ['orders.model.lkml', 'views/a.view.lkml', 'views/sub/b.view.lkml'], }); const graph = buildLookmlGraph(project); // Single-star glob matches one path segment — "views/sub/b.view.lkml" is NOT matched. expect(graph.viewsIncludedByModel.get('orders')?.sort()).toEqual(['views/a.view.lkml']); }); it('resolves double-star include patterns (views/**/*.view.lkml) recursively', () => { const project = mkProject({ models: [{ path: 'orders.model.lkml', name: 'orders', includes: ['views/**/*.view.lkml'], explores: [] }], views: [ { path: 'views/a.view.lkml', name: 'a', extendsFrom: [] }, { path: 'views/sub/b.view.lkml', name: 'b', extendsFrom: [] }, ], allPaths: ['orders.model.lkml', 'views/a.view.lkml', 'views/sub/b.view.lkml'], }); const graph = buildLookmlGraph(project); expect(graph.viewsIncludedByModel.get('orders')?.sort()).toEqual(['views/a.view.lkml', 'views/sub/b.view.lkml']); }); it('leaves a view ownerless when no model includes it', () => { const project = mkProject({ models: [{ path: 'other.model.lkml', name: 'other', includes: ['views/included.view.lkml'], explores: [] }], views: [ { path: 'views/included.view.lkml', name: 'included', extendsFrom: [] }, { path: 'views/orphan.view.lkml', name: 'orphan', extendsFrom: [] }, ], allPaths: ['other.model.lkml', 'views/included.view.lkml', 'views/orphan.view.lkml'], }); const graph = buildLookmlGraph(project); expect(graph.ownerByViewPath.has('views/orphan.view.lkml')).toBe(false); expect(graph.ownerByViewPath.get('views/included.view.lkml')).toBe('other'); }); });