Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1960,6 +1960,100 @@ describe('[example] interpreter', () => {
});
});

describe('DiagramView with schema-qualified (infix) table references', () => {
test('should resolve schema-qualified table in Tables block', () => {
const source = `
Table ecommerce.merchants { id int }
DiagramView myView {
Tables { ecommerce.merchants }
}
`;
const db = interpret(source).getValue()!;
const ve = db.diagramViews[0].visibleEntities;
expect(ve.tables).toEqual([
{ name: 'merchants', schemaName: 'ecommerce' },
]);
});

test('should resolve mixed schema-qualified and simple table refs', () => {
const source = `
Table users { id int }
Table ecommerce.orders { id int }
Table ecommerce.merchants { id int }
DiagramView myView {
Tables {
ecommerce.merchants
users
ecommerce.orders
}
}
`;
const db = interpret(source).getValue()!;
const ve = db.diagramViews[0].visibleEntities;
expect(ve.tables).toEqual([
{ name: 'merchants', schemaName: 'ecommerce' },
{ name: 'users', schemaName: 'public' },
{ name: 'orders', schemaName: 'ecommerce' },
]);
});

test('should resolve multiple DiagramViews with schema-qualified refs', () => {
const source = `
Table users { id int }
Table ecommerce.orders { id int }
Table ecommerce.merchants { id int }
DiagramView Default {
Tables {
ecommerce.merchants
users
}
}
DiagramView "New View" {
Tables {
ecommerce.orders
}
}
`;
const db = interpret(source).getValue()!;
expect(db.diagramViews).toHaveLength(2);
expect(db.diagramViews[0].visibleEntities.tables).toEqual([
{ name: 'merchants', schemaName: 'ecommerce' },
{ name: 'users', schemaName: 'public' },
]);
expect(db.diagramViews[1].visibleEntities.tables).toEqual([
{ name: 'orders', schemaName: 'ecommerce' },
]);
});

test('should resolve public schema-qualified table ref', () => {
const source = `
Table a { id int }
DiagramView myView {
Tables { public.a }
}
`;
const db = interpret(source).getValue()!;
const ve = db.diagramViews[0].visibleEntities;
expect(ve.tables).toEqual([
{ name: 'a', schemaName: 'public' },
]);
});

test('should resolve schema-qualified alias in Tables block', () => {
const source = `
Table ecommerce.merchants as M { id int }
DiagramView myView {
Tables { M }
}
`;
const db = interpret(source).getValue()!;
const ve = db.diagramViews[0].visibleEntities;
expect(ve.tables).toEqual([
{ name: 'merchants', schemaName: 'ecommerce' },
]);
});
});

describe('standalone note interpretation', () => {
test('should interpret standalone note', () => {
const source = `
Expand Down
11 changes: 1 addition & 10 deletions packages/dbml-parse/src/compiler/queries/resolutionIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
SchemaSymbol,
SymbolKind,
} from '@/core/types/symbol';
import { isAccessExpression, isExpressionAVariableNode } from '@/core/utils/validate';
import { getRightmostVariable, isAccessExpression, isExpressionAVariableNode } from '@/core/utils/validate';
import type Compiler from '../index';

export interface ResolutionIndex {
Expand All @@ -22,15 +22,6 @@ export interface ResolutionIndex {
parents: Map<InternedNodeSymbol, NodeSymbol[]>;
}

function getRightmostVariable (node: SyntaxNode): SyntaxNode | undefined {
if (isExpressionAVariableNode(node)) return node;
if (isAccessExpression(node)) {
const right = (node as InfixExpressionNode).rightExpression;
if (right && isExpressionAVariableNode(right)) return right;
}
return undefined;
}

// Build full resolution index: references + metadata + symbol parent. One scan of all files.
export function resolutionIndex (this: Compiler): ResolutionIndex {
const references = new Map<InternedNodeSymbol, SyntaxNode[]>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
SymbolKind,
} from '@/core/types/symbol';
import { destructureComplexVariable } from '@/core/utils/expression';
import { isWildcardExpression } from '@/core/utils/validate';
import { getRightmostVariable, isWildcardExpression } from '@/core/utils/validate';

export class DiagramViewInterpreter {
private compiler: Compiler;
Expand Down Expand Up @@ -184,8 +184,11 @@ export class DiagramViewInterpreter {
if (!(field instanceof FunctionApplicationNode)) continue;

// If the field was bound to a symbol (e.g., alias "U" -> Table "users"),
// resolve the real name from the referee's declaration
const referee = field.callee && this.compiler.nodeReferee(field.callee!).getFiltered(UNHANDLED);
// resolve the real name from the referee's declaration.
// For access expressions (e.g. ecommerce.merchants), extract the rightmost
// variable node since nodeReferee expects a PrimaryExpressionNode.
const refereeNode = field.callee && getRightmostVariable(field.callee);
const referee = refereeNode && this.compiler.nodeReferee(refereeNode).getFiltered(UNHANDLED);
if (referee?.declaration instanceof ElementDeclarationNode) {
const realFragments = destructureComplexVariable(referee.declaration.name) ?? [];
if (realFragments.length > 0) {
Expand Down
12 changes: 12 additions & 0 deletions packages/dbml-parse/src/core/utils/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,18 @@ export function isValidIndexName (
);
}

// Extracts the rightmost variable node from an expression.
// For a simple variable node, returns itself. For an access expression (e.g. `ecommerce.merchants`),
// returns the rightmost fragment (`merchants`) if it is a variable node.
export function getRightmostVariable (node: SyntaxNode): SyntaxNode | undefined {
if (isExpressionAVariableNode(node)) return node;
if (isAccessExpression(node)) {
const right = (node as InfixExpressionNode).rightExpression;
if (right && isExpressionAVariableNode(right)) return right;
}
return undefined;
}

// Returns true if `node` is the rightmost (terminal) fragment of an access expression chain.
// A node is terminal when its containing access expression is NOT itself the left-hand side
// of a further access expression (e.g. `users` in `public.users` or `auth.public.users`).
Expand Down
Loading