diff --git a/packages/dbml-core/types/model_structure/index.d.ts b/packages/dbml-core/types/model_structure/index.d.ts index 0f98412bb..83dc4f4a0 100644 --- a/packages/dbml-core/types/model_structure/index.d.ts +++ b/packages/dbml-core/types/model_structure/index.d.ts @@ -14,6 +14,7 @@ export { default as TableGroup } from './tableGroup'; export { default as StickyNote } from './stickyNote'; export { default as Check } from './check'; export { default as TablePartial } from './tablePartial'; +export { default as TableRecord } from './records'; export * from './database'; export * from './schema'; @@ -30,3 +31,4 @@ export * from './stickyNote'; export * from './tablePartial'; export * from './check'; export * from './element'; +export * from './records'; diff --git a/packages/dbml-core/types/model_structure/records.d.ts b/packages/dbml-core/types/model_structure/records.d.ts new file mode 100644 index 000000000..1d2d52483 --- /dev/null +++ b/packages/dbml-core/types/model_structure/records.d.ts @@ -0,0 +1,26 @@ +export type RecordValueType = 'string' | 'bool' | 'integer' | 'real' | 'date' | 'time' | 'datetime' | string; + +export interface RecordValue { + value: any; + type: RecordValueType; +} + +export interface RawTableRecord { + schemaName: string | undefined; + tableName: string; + columns: string[]; + values: { + value: any; + type: RecordValueType; + }[][]; +} + +export default interface TableRecord extends RawTableRecord { + id: number; +} + +export type NormalizedRecord = TableRecord; + +export interface NormalizedRecordIdMap { + [_id: number]: NormalizedRecord; +} diff --git a/packages/dbml-parse/__tests__/examples/binder/binder.test.ts b/packages/dbml-parse/__tests__/examples/binder/binder.test.ts index 9fb7fde87..283ab7080 100644 --- a/packages/dbml-parse/__tests__/examples/binder/binder.test.ts +++ b/packages/dbml-parse/__tests__/examples/binder/binder.test.ts @@ -1,35 +1,65 @@ -import { describe, expect } from 'vitest'; -import { SyntaxNodeKind, ElementDeclarationNode, BlockExpressionNode } from '@/core/parser/nodes'; -import { TableSymbol, EnumSymbol, TableGroupSymbol, TablePartialSymbol, ColumnSymbol, EnumFieldSymbol, SchemaSymbol } from '@/core/analyzer/symbol/symbols'; -import { analyze } from '@tests/utils'; +import { describe, expect, test } from 'vitest'; +import { SyntaxNodeKind, ElementDeclarationNode, BlockExpressionNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { UNHANDLED } from '@/constants'; +import { Compiler } from '@/index'; + +function findMember (compiler: Compiler, parentSymbol: NodeSymbol, name: string, kind: SymbolKind): NodeSymbol | undefined { + const members = compiler.symbolMembers(parentSymbol); + if (members.hasValue(UNHANDLED)) return undefined; + return members.getValue().find((m) => { + if (!m.isKind(kind)) return false; + // SchemaSymbol has a name property directly + if ('name' in m && typeof (m as any).name === 'string') return (m as any).name === name; + if (!m.declaration) return false; + const fn = compiler.fullname(m.declaration); + if (fn.hasValue(UNHANDLED)) return false; + return fn.getValue()?.at(-1) === name; + }); +} + +function createCompilerAndAnalyze (source: string): { compiler: Compiler; ast: any; errors: any[] } { + const compiler = new Compiler(); + compiler.setSource(source); + const parseResult = compiler.parseFile(); + const { ast } = parseResult.getValue(); + const bindResult = compiler.bind(ast); + const validateResult = compiler.validate(ast); + const errors = [...parseResult.getErrors(), ...bindResult.getErrors(), ...validateResult.getErrors()]; + return { compiler, ast, errors }; +} describe('[example] binder', () => { describe('Table', () => { test('should create TableSymbol with correct properties', () => { - const ast = analyze('Table users { id int }').getValue(); + const compiler = new Compiler(); + compiler.setSource('Table users { id int }'); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); const tableNode = elements[0]; - const tableSymbol = tableNode.symbol as TableSymbol; + const tableSymbol = compiler.nodeSymbol(tableNode).getValue(); // Verify symbol properties - expect(tableSymbol).toBeInstanceOf(TableSymbol); + expect(tableSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); expect(tableSymbol.declaration).toBe(tableNode); - expect(tableSymbol.references).toEqual([]); + expect(compiler.symbolReferences(tableSymbol).getValue()).toEqual([]); - // Verify symbolTable contains column - expect(tableSymbol.symbolTable.get('Column:id')).toBeInstanceOf(ColumnSymbol); + // Verify symbolMembers contains column + const idColumn = findMember(compiler, tableSymbol, 'id', SymbolKind.Column); + expect(idColumn).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); // Verify column symbol properties - const columnSymbol = tableSymbol.symbolTable.get('Column:id') as ColumnSymbol; const tableBody = tableNode.body as BlockExpressionNode; const columnNode = tableBody.body[0]; - expect(columnSymbol.declaration).toBe(columnNode); - expect(columnSymbol.references).toEqual([]); - - // Verify public schema symbol table (publicSymbolTable concept) - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol).toBeInstanceOf(SchemaSymbol); - expect(schemaSymbol.symbolTable.get('Table:users')).toBe(tableSymbol); + expect(idColumn!.declaration).toBe(columnNode); + expect(compiler.symbolReferences(idColumn!).getValue()).toEqual([]); + + // Verify program symbol table + const programSymbol = compiler.nodeSymbol(ast).getValue(); + expect(programSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Program)); + const usersFromProgram = findMember(compiler, programSymbol, 'users', SymbolKind.Table); + expect(usersFromProgram).toBe(tableSymbol); }); test('should verify nested children symbol properties', () => { @@ -40,27 +70,31 @@ describe('[example] binder', () => { email varchar } `; - const ast = analyze(source).getValue(); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const tableNode = ast.body[0] as ElementDeclarationNode; - const tableSymbol = tableNode.symbol as TableSymbol; + const tableSymbol = compiler.nodeSymbol(tableNode).getValue(); const tableBody = tableNode.body as BlockExpressionNode; - // Verify all columns are in symbolTable - expect(tableSymbol.symbolTable.get('Column:id')).toBeInstanceOf(ColumnSymbol); - expect(tableSymbol.symbolTable.get('Column:name')).toBeInstanceOf(ColumnSymbol); - expect(tableSymbol.symbolTable.get('Column:email')).toBeInstanceOf(ColumnSymbol); + // Verify all columns are in members + expect(findMember(compiler, tableSymbol, 'id', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, tableSymbol, 'name', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, tableSymbol, 'email', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); // Verify each column's symbol and declaration relationship + const expectedNames = ['id', 'name', 'email']; tableBody.body.forEach((field, index) => { const columnNode = field as ElementDeclarationNode; - const columnSymbol = columnNode.symbol as ColumnSymbol; + const columnSymbol = compiler.nodeSymbol(columnNode).getValue(); - expect(columnSymbol).toBeInstanceOf(ColumnSymbol); + expect(columnSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); expect(columnSymbol.declaration).toBe(columnNode); - // Verify column is accessible from table's symbolTable - const expectedNames = ['id', 'name', 'email']; - expect(tableSymbol.symbolTable.get(`Column:${expectedNames[index]}`)).toBe(columnSymbol); + // Verify column is accessible from table's members + const found = findMember(compiler, tableSymbol, expectedNames[index], SymbolKind.Column); + expect(found).toBe(columnSymbol); }); }); @@ -69,20 +103,19 @@ describe('[example] binder', () => { Table users { id int\n name varchar } Table posts { id int\n name varchar } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); - - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('Table:users')).toBeInstanceOf(TableSymbol); - expect(schemaSymbol.symbolTable.get('Table:posts')).toBeInstanceOf(TableSymbol); - - const usersSymbol = schemaSymbol.symbolTable.get('Table:users') as TableSymbol; - const postsSymbol = schemaSymbol.symbolTable.get('Table:posts') as TableSymbol; - expect(usersSymbol.symbolTable.get('Column:id')).toBeInstanceOf(ColumnSymbol); - expect(usersSymbol.symbolTable.get('Column:name')).toBeInstanceOf(ColumnSymbol); - expect(postsSymbol.symbolTable.get('Column:id')).toBeInstanceOf(ColumnSymbol); - expect(postsSymbol.symbolTable.get('Column:name')).toBeInstanceOf(ColumnSymbol); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); + + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const usersSymbol = findMember(compiler, programSymbol, 'users', SymbolKind.Table)!; + const postsSymbol = findMember(compiler, programSymbol, 'posts', SymbolKind.Table)!; + expect(usersSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); + expect(postsSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); + + expect(findMember(compiler, usersSymbol, 'id', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, usersSymbol, 'name', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, postsSymbol, 'id', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, postsSymbol, 'name', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); }); test('should detect duplicate table names within same schema', () => { @@ -90,7 +123,7 @@ describe('[example] binder', () => { Table users { id int } Table users { email varchar } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(1); expect(errors[0].diagnostic).toBe("Table name 'users' already exists in schema 'public'"); @@ -101,19 +134,23 @@ describe('[example] binder', () => { Table auth.users { id int } Table public.users { id int } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); + + const programSymbol = compiler.nodeSymbol(ast).getValue(); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; + // Root has auth schema and public schema + const authSchema = findMember(compiler, programSymbol, 'auth', SymbolKind.Schema)!; + expect(authSchema).toSatisfy((s: any) => s.isKind(SymbolKind.Schema)); + const publicSchema = findMember(compiler, programSymbol, 'public', SymbolKind.Schema)!; + expect(publicSchema).toSatisfy((s: any) => s.isKind(SymbolKind.Schema)); - // Root has auth schema and public.users table - expect(schemaSymbol.symbolTable.get('Schema:auth')).toBeInstanceOf(SchemaSymbol); - expect(schemaSymbol.symbolTable.get('Table:users')).toBeInstanceOf(TableSymbol); + // public schema has users table + const publicUsers = findMember(compiler, publicSchema, 'users', SymbolKind.Table)!; + expect(publicUsers).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); // auth schema has users table - const authSchema = schemaSymbol.symbolTable.get('Schema:auth') as SchemaSymbol; - expect(authSchema.symbolTable.get('Table:users')).toBeInstanceOf(TableSymbol); + expect(findMember(compiler, authSchema, 'users', SymbolKind.Table)).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); }); test('should handle table aliases', () => { @@ -122,18 +159,17 @@ describe('[example] binder', () => { TableGroup g1 { U } Ref: U.id < U.id `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); - const usersSymbol = elements[0].symbol as TableSymbol; + const usersSymbol = compiler.nodeSymbol(elements[0]).getValue(); - expect(usersSymbol.references.length).toBe(3); + expect(compiler.symbolReferences(usersSymbol).getValue().length).toBe(3); // 1 from TableGroup, 2 from Ref (U.id appears twice) - usersSymbol.references.forEach((refNode) => { + compiler.symbolReferences(usersSymbol).getValue().forEach((refNode) => { expect(refNode.kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(refNode.referee).toBe(usersSymbol); + expect(compiler.nodeReferee(refNode).getValue()).toBe(usersSymbol); }); }); @@ -144,47 +180,50 @@ describe('[example] binder', () => { manager_id int [ref: > employees.id] } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); - const employeesSymbol = elements[0].symbol as TableSymbol; + const employeesSymbol = compiler.nodeSymbol(elements[0]).getValue(); - expect(employeesSymbol.references.length).toBe(1); - expect(employeesSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(employeesSymbol.references[0].referee).toBe(employeesSymbol); + expect(compiler.symbolReferences(employeesSymbol).getValue().length).toBe(1); + expect(compiler.symbolReferences(employeesSymbol).getValue()[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(compiler.symbolReferences(employeesSymbol).getValue()[0]).getValue()).toBe(employeesSymbol); }); test('should handle deeply nested schema names and quoted identifiers', () => { - const result1 = analyze('Table a.b.c { id int }'); - expect(result1.getErrors()).toHaveLength(0); - const schemaSymbol1 = result1.getValue().symbol as SchemaSymbol; - expect(schemaSymbol1.symbolTable.get('Schema:a')).toBeInstanceOf(SchemaSymbol); - - const result2 = analyze('Table "user-table" { "user-id" int }'); - expect(result2.getErrors()).toHaveLength(0); - const schemaSymbol2 = result2.getValue().symbol as SchemaSymbol; - expect(schemaSymbol2.symbolTable.get('Table:user-table')).toBeInstanceOf(TableSymbol); + const { compiler: compiler1, ast: ast1, errors: errors1 } = createCompilerAndAnalyze('Table a.b.c { id int }'); + expect(errors1).toHaveLength(0); + const programSymbol1 = compiler1.nodeSymbol(ast1).getValue(); + expect(findMember(compiler1, programSymbol1, 'a', SymbolKind.Schema)).toSatisfy((s: any) => s.isKind(SymbolKind.Schema)); + + const { compiler: compiler2, ast: ast2, errors: errors2 } = createCompilerAndAnalyze('Table "user-table" { "user-id" int }'); + expect(errors2).toHaveLength(0); + const programSymbol2 = compiler2.nodeSymbol(ast2).getValue(); + expect(findMember(compiler2, programSymbol2, 'user-table', SymbolKind.Table)).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); }); }); describe('Column', () => { test('should create ColumnSymbol with correct properties', () => { const source = 'Table users { id int [pk] }'; - const ast = analyze(source).getValue(); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const tableElement = ast.body[0] as ElementDeclarationNode; const tableBody = tableElement.body as BlockExpressionNode; const columnNode = tableBody.body[0] as ElementDeclarationNode; - const columnSymbol = columnNode.symbol as ColumnSymbol; + const columnSymbol = compiler.nodeSymbol(columnNode).getValue(); - expect(columnSymbol).toBeInstanceOf(ColumnSymbol); + expect(columnSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); expect(columnSymbol.declaration).toBe(columnNode); - expect(columnSymbol.references).toEqual([]); + expect(compiler.symbolReferences(columnSymbol).getValue()).toEqual([]); - // Verify column is in table's symbol table - const tableSymbol = tableElement.symbol as TableSymbol; - expect(tableSymbol.symbolTable.get('Column:id')).toBe(columnSymbol); + // Verify column is in table's members + const tableSymbol = compiler.nodeSymbol(tableElement).getValue(); + const idColumn = findMember(compiler, tableSymbol, 'id', SymbolKind.Column); + expect(idColumn).toBe(columnSymbol); }); test('should detect duplicate column names in same table', () => { @@ -195,7 +234,7 @@ describe('[example] binder', () => { id varchar } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(2); expect(errors[0].diagnostic).toBe('Duplicate column id'); @@ -211,17 +250,16 @@ describe('[example] binder', () => { status varchar [not null, unique] } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const tableElement = ast.body[0] as ElementDeclarationNode; - const tableSymbol = tableElement.symbol as TableSymbol; + const tableSymbol = compiler.nodeSymbol(tableElement).getValue(); - expect(tableSymbol.symbolTable.get('Column:id')).toBeInstanceOf(ColumnSymbol); - expect(tableSymbol.symbolTable.get('Column:name')).toBeInstanceOf(ColumnSymbol); - expect(tableSymbol.symbolTable.get('Column:email')).toBeInstanceOf(ColumnSymbol); - expect(tableSymbol.symbolTable.get('Column:status')).toBeInstanceOf(ColumnSymbol); + expect(findMember(compiler, tableSymbol, 'id', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, tableSymbol, 'name', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, tableSymbol, 'email', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, tableSymbol, 'status', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); }); test('should track column references from inline refs', () => { @@ -229,16 +267,20 @@ describe('[example] binder', () => { Table users { id int [pk] } Table posts { user_id int [ref: > users.id] } `; - const ast = analyze(source).getValue(); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); const usersTable = elements[0]; const tableBody = usersTable.body as BlockExpressionNode; const idColumn = tableBody.body[0] as ElementDeclarationNode; - const columnSymbol = idColumn.symbol as ColumnSymbol; + const columnSymbol = compiler.nodeSymbol(idColumn).getValue(); - expect(columnSymbol.references.length).toBe(1); - expect(columnSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(columnSymbol.references[0].referee).toBe(columnSymbol); + const refs = compiler.symbolReferences(columnSymbol).getValue(); + expect(refs.length).toBe(1); + expect(refs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(refs[0]).getValue()).toBe(columnSymbol); }); test('should maintain correct reference counts after multiple refs', () => { @@ -248,24 +290,29 @@ describe('[example] binder', () => { Table comments { user_id int [ref: > users.id] } Table likes { user_id int [ref: > users.id] } `; - const ast = analyze(source).getValue(); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); const usersTable = elements[0]; - const usersSymbol = usersTable.symbol as TableSymbol; + const usersSymbol = compiler.nodeSymbol(usersTable).getValue(); const tableBody = usersTable.body as BlockExpressionNode; const idColumn = tableBody.body[0] as ElementDeclarationNode; - const columnSymbol = idColumn.symbol as ColumnSymbol; + const columnSymbol = compiler.nodeSymbol(idColumn).getValue(); - expect(usersSymbol.references.length).toBe(3); - usersSymbol.references.forEach((refNode) => { + const usersRefs = compiler.symbolReferences(usersSymbol).getValue(); + expect(usersRefs.length).toBe(3); + usersRefs.forEach((refNode) => { expect(refNode.kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(refNode.referee).toBe(usersSymbol); + expect(compiler.nodeReferee(refNode).getValue()).toBe(usersSymbol); }); - expect(columnSymbol.references.length).toBe(3); - columnSymbol.references.forEach((refNode) => { + const colRefs = compiler.symbolReferences(columnSymbol).getValue(); + expect(colRefs.length).toBe(3); + colRefs.forEach((refNode) => { expect(refNode.kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(refNode.referee).toBe(columnSymbol); + expect(compiler.nodeReferee(refNode).getValue()).toBe(columnSymbol); }); }); }); @@ -281,13 +328,12 @@ describe('[example] binder', () => { } } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const tableSymbol = (ast.body[0] as ElementDeclarationNode).symbol as TableSymbol; - expect(tableSymbol.symbolTable.get('Column:id')).toBeInstanceOf(ColumnSymbol); - expect(tableSymbol.symbolTable.get('Column:email')).toBeInstanceOf(ColumnSymbol); + const tableSymbol = compiler.nodeSymbol(ast.body[0] as ElementDeclarationNode).getValue(); + expect(findMember(compiler, tableSymbol, 'id', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, tableSymbol, 'email', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); }); test('should detect unknown columns in indexes', () => { @@ -299,10 +345,10 @@ describe('[example] binder', () => { } } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(1); - expect(errors[0].diagnostic).toBe("No column named 'nonexistent_column' inside Table 'users'"); + expect(errors[0].diagnostic).toBe("Column 'nonexistent_column' does not exist in Table 'users'"); }); test('should bind composite indexes with settings', () => { @@ -317,14 +363,13 @@ describe('[example] binder', () => { } } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); - - const ast = result.getValue(); - const tableSymbol = (ast.body[0] as ElementDeclarationNode).symbol as TableSymbol; - expect(tableSymbol.symbolTable.get('Column:first_name')).toBeInstanceOf(ColumnSymbol); - expect(tableSymbol.symbolTable.get('Column:last_name')).toBeInstanceOf(ColumnSymbol); - expect(tableSymbol.symbolTable.get('Column:email')).toBeInstanceOf(ColumnSymbol); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); + + const tableSymbol = compiler.nodeSymbol(ast.body[0] as ElementDeclarationNode).getValue(); + expect(findMember(compiler, tableSymbol, 'first_name', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, tableSymbol, 'last_name', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(findMember(compiler, tableSymbol, 'email', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); }); }); @@ -336,20 +381,24 @@ describe('[example] binder', () => { inactive } `; - const ast = analyze(source).getValue(); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); const enumNode = elements[0]; - const enumSymbol = enumNode.symbol as EnumSymbol; + const enumSymbol = compiler.nodeSymbol(enumNode).getValue(); - expect(enumSymbol).toBeInstanceOf(EnumSymbol); + expect(enumSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Enum)); expect(enumSymbol.declaration).toBe(enumNode); - expect(enumSymbol.symbolTable.get('Enum field:active')).toBeInstanceOf(EnumFieldSymbol); - expect(enumSymbol.symbolTable.get('Enum field:inactive')).toBeInstanceOf(EnumFieldSymbol); - expect(enumSymbol.references).toEqual([]); - - // Verify enum is in public schema symbol table - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('Enum:status')).toBe(enumSymbol); + expect(findMember(compiler, enumSymbol, 'active', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); + expect(findMember(compiler, enumSymbol, 'inactive', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); + expect(compiler.symbolReferences(enumSymbol).getValue()).toEqual([]); + + // Verify enum is in program symbol members + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const statusEnum = findMember(compiler, programSymbol, 'status', SymbolKind.Enum); + expect(statusEnum).toBe(enumSymbol); }); test('should create EnumFieldSymbol with correct properties', () => { @@ -360,23 +409,22 @@ describe('[example] binder', () => { rejected } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const enumElement = ast.body[0] as ElementDeclarationNode; - const enumSymbol = enumElement.symbol as EnumSymbol; + const enumSymbol = compiler.nodeSymbol(enumElement).getValue(); - expect(enumSymbol.symbolTable.get('Enum field:pending')).toBeInstanceOf(EnumFieldSymbol); - expect(enumSymbol.symbolTable.get('Enum field:approved')).toBeInstanceOf(EnumFieldSymbol); - expect(enumSymbol.symbolTable.get('Enum field:rejected')).toBeInstanceOf(EnumFieldSymbol); + expect(findMember(compiler, enumSymbol, 'pending', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); + expect(findMember(compiler, enumSymbol, 'approved', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); + expect(findMember(compiler, enumSymbol, 'rejected', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); const enumBody = enumElement.body as BlockExpressionNode; enumBody.body.forEach((field) => { - const fieldSymbol = (field as ElementDeclarationNode).symbol as EnumFieldSymbol; - expect(fieldSymbol).toBeInstanceOf(EnumFieldSymbol); + const fieldSymbol = compiler.nodeSymbol(field as ElementDeclarationNode).getValue(); + expect(fieldSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); expect(fieldSymbol.declaration).toBe(field); - expect(fieldSymbol.references).toEqual([]); + expect(compiler.symbolReferences(fieldSymbol).getValue()).toEqual([]); }); }); @@ -387,7 +435,7 @@ describe('[example] binder', () => { active } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(2); expect(errors[0].diagnostic).toBe('Duplicate enum field active'); @@ -399,18 +447,17 @@ describe('[example] binder', () => { Enum a { val1\n val2 } Enum b { val1\n val2 } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); - - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - - const enumA = schemaSymbol.symbolTable.get('Enum:a') as EnumSymbol; - const enumB = schemaSymbol.symbolTable.get('Enum:b') as EnumSymbol; - expect(enumA.symbolTable.get('Enum field:val1')).toBeInstanceOf(EnumFieldSymbol); - expect(enumA.symbolTable.get('Enum field:val2')).toBeInstanceOf(EnumFieldSymbol); - expect(enumB.symbolTable.get('Enum field:val1')).toBeInstanceOf(EnumFieldSymbol); - expect(enumB.symbolTable.get('Enum field:val2')).toBeInstanceOf(EnumFieldSymbol); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); + + const programSymbol = compiler.nodeSymbol(ast).getValue(); + + const enumA = findMember(compiler, programSymbol, 'a', SymbolKind.Enum)!; + const enumB = findMember(compiler, programSymbol, 'b', SymbolKind.Enum)!; + expect(findMember(compiler, enumA, 'val1', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); + expect(findMember(compiler, enumA, 'val2', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); + expect(findMember(compiler, enumB, 'val1', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); + expect(findMember(compiler, enumB, 'val2', SymbolKind.EnumField)).toSatisfy((s: any) => s.isKind(SymbolKind.EnumField)); }); test('should allow enum type reference in column', () => { @@ -424,13 +471,12 @@ describe('[example] binder', () => { status status } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('Enum:status')).toBeInstanceOf(EnumSymbol); - expect(schemaSymbol.symbolTable.get('Table:users')).toBeInstanceOf(TableSymbol); + const programSymbol = compiler.nodeSymbol(ast).getValue(); + expect(findMember(compiler, programSymbol, 'status', SymbolKind.Enum)).toSatisfy((s: any) => s.isKind(SymbolKind.Enum)); + expect(findMember(compiler, programSymbol, 'users', SymbolKind.Table)).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); }); test('should allow enum from different schema', () => { @@ -444,13 +490,12 @@ describe('[example] binder', () => { status types.status } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const typesSchema = schemaSymbol.symbolTable.get('Schema:types') as SchemaSymbol; - expect(typesSchema.symbolTable.get('Enum:status')).toBeInstanceOf(EnumSymbol); + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const typesSchema = findMember(compiler, programSymbol, 'types', SymbolKind.Schema)!; + expect(findMember(compiler, typesSchema, 'status', SymbolKind.Enum)).toSatisfy((s: any) => s.isKind(SymbolKind.Enum)); }); test('should allow forward reference to enum', () => { @@ -460,13 +505,12 @@ describe('[example] binder', () => { } Enum status_enum { active\n inactive } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('Table:users')).toBeInstanceOf(TableSymbol); - expect(schemaSymbol.symbolTable.get('Enum:status_enum')).toBeInstanceOf(EnumSymbol); + const programSymbol = compiler.nodeSymbol(ast).getValue(); + expect(findMember(compiler, programSymbol, 'users', SymbolKind.Table)).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); + expect(findMember(compiler, programSymbol, 'status_enum', SymbolKind.Enum)).toSatisfy((s: any) => s.isKind(SymbolKind.Enum)); }); test('should bind enum field references in default values', () => { @@ -481,19 +525,19 @@ describe('[example] binder', () => { status order_status [default: order_status.pending] } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const enumSymbol = schemaSymbol.symbolTable.get('Enum:order_status') as EnumSymbol; - const pendingField = enumSymbol.symbolTable.get('Enum field:pending') as EnumFieldSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const enumSymbol = findMember(compiler, programSymbol, 'order_status', SymbolKind.Enum)!; + const pendingField = findMember(compiler, enumSymbol, 'pending', SymbolKind.EnumField)!; // Enum should have 2 references: column type + default value - expect(enumSymbol.references.length).toBe(2); + expect(compiler.symbolReferences(enumSymbol).getValue().length).toBe(2); // Enum field should have 1 reference from default value - expect(pendingField.references.length).toBe(1); - expect(pendingField.references[0].referee).toBe(pendingField); + const pendingRefs = compiler.symbolReferences(pendingField).getValue(); + expect(pendingRefs.length).toBe(1); + expect(compiler.nodeReferee(pendingRefs[0]).getValue()).toBe(pendingField); }); test('should bind schema-qualified enum field references in default values', () => { @@ -506,18 +550,18 @@ describe('[example] binder', () => { status types.status [default: types.status.active] } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); - - const ast = result.getValue(); - const publicSchema = ast.symbol as SchemaSymbol; - const typesSchema = publicSchema.symbolTable.get('Schema:types') as SchemaSymbol; - const enumSymbol = typesSchema.symbolTable.get('Enum:status') as EnumSymbol; - const activeField = enumSymbol.symbolTable.get('Enum field:active') as EnumFieldSymbol; - - expect(enumSymbol.references.length).toBe(2); - expect(activeField.references.length).toBe(1); - expect(activeField.references[0].referee).toBe(activeField); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); + + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const typesSchema = findMember(compiler, programSymbol, 'types', SymbolKind.Schema)!; + const enumSymbol = findMember(compiler, typesSchema, 'status', SymbolKind.Enum)!; + const activeField = findMember(compiler, enumSymbol, 'active', SymbolKind.EnumField)!; + + expect(compiler.symbolReferences(enumSymbol).getValue().length).toBe(2); + const activeRefs = compiler.symbolReferences(activeField).getValue(); + expect(activeRefs.length).toBe(1); + expect(compiler.nodeReferee(activeRefs[0]).getValue()).toBe(activeField); }); test('should detect invalid enum field in default value', () => { @@ -527,7 +571,7 @@ describe('[example] binder', () => { status status [default: status.nonexistent] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Enum field 'nonexistent' does not exist in Enum 'status'"); }); @@ -538,7 +582,7 @@ describe('[example] binder', () => { status varchar [default: nonexistent_enum.value] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Enum 'nonexistent_enum' does not exist in Schema 'public'"); }); @@ -549,7 +593,7 @@ describe('[example] binder', () => { name varchar [default: "hello"] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(0); }); @@ -559,7 +603,7 @@ describe('[example] binder', () => { name varchar [default: \`hello\`] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(0); }); @@ -569,7 +613,7 @@ describe('[example] binder', () => { name varchar [default: null] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(0); }); @@ -579,7 +623,7 @@ describe('[example] binder', () => { active boolean [default: true] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(0); }); @@ -589,7 +633,7 @@ describe('[example] binder', () => { active boolean [default: false] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(0); }); @@ -600,7 +644,7 @@ describe('[example] binder', () => { status varchar [default: true.value] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Enum 'true' does not exist in Schema 'public'"); }); @@ -614,7 +658,7 @@ describe('[example] binder', () => { status true [default: true.value] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Enum field 'value' does not exist in Enum 'true'"); }); @@ -629,19 +673,18 @@ describe('[example] binder', () => { status true [default: true.value] } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); // Verify the binding - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const enumSymbol = schemaSymbol.symbolTable.get('Enum:true') as EnumSymbol; - const valueField = enumSymbol.symbolTable.get('Enum field:value') as EnumFieldSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const enumSymbol = findMember(compiler, programSymbol, 'true', SymbolKind.Enum)!; + const valueField = findMember(compiler, enumSymbol, 'value', SymbolKind.EnumField)!; // Enum should have 2 references: column type + default value - expect(enumSymbol.references.length).toBe(2); + expect(compiler.symbolReferences(enumSymbol).getValue().length).toBe(2); // Enum field should have 1 reference from default value - expect(valueField.references.length).toBe(1); + expect(compiler.symbolReferences(valueField).getValue().length).toBe(1); }); test('should bind quoted string with field as enum access', () => { @@ -651,7 +694,7 @@ describe('[example] binder', () => { status varchar [default: "hello".abc] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Enum 'hello' does not exist in Schema 'public'"); }); @@ -664,21 +707,22 @@ describe('[example] binder', () => { Table posts { user_id int } Ref: posts.user_id > users.id `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); - const usersSymbol = elements[0].symbol as TableSymbol; - const postsSymbol = elements[1].symbol as TableSymbol; - - expect(usersSymbol.references.length).toBe(1); - expect(usersSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(usersSymbol.references[0].referee).toBe(usersSymbol); - - expect(postsSymbol.references.length).toBe(1); - expect(postsSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(postsSymbol.references[0].referee).toBe(postsSymbol); + const usersSymbol = compiler.nodeSymbol(elements[0]).getValue(); + const postsSymbol = compiler.nodeSymbol(elements[1]).getValue(); + + const usersRefs = compiler.symbolReferences(usersSymbol).getValue(); + expect(usersRefs.length).toBe(1); + expect(usersRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(usersRefs[0]).getValue()).toBe(usersSymbol); + + const postsRefs = compiler.symbolReferences(postsSymbol).getValue(); + expect(postsRefs.length).toBe(1); + expect(postsRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(postsRefs[0]).getValue()).toBe(postsSymbol); }); test('should bind inline refs', () => { @@ -686,28 +730,29 @@ describe('[example] binder', () => { Table users { id int [pk] } Table posts { user_id int [ref: > users.id] } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); const usersTable = elements[0]; - const usersSymbol = usersTable.symbol as TableSymbol; + const usersSymbol = compiler.nodeSymbol(usersTable).getValue(); const tableBody = usersTable.body as BlockExpressionNode; const idColumn = tableBody.body[0] as ElementDeclarationNode; - const columnSymbol = idColumn.symbol as ColumnSymbol; + const columnSymbol = compiler.nodeSymbol(idColumn).getValue(); - expect(usersSymbol.references.length).toBe(1); - expect(usersSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(usersSymbol.references[0].referee).toBe(usersSymbol); + const usersRefs = compiler.symbolReferences(usersSymbol).getValue(); + expect(usersRefs.length).toBe(1); + expect(usersRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(usersRefs[0]).getValue()).toBe(usersSymbol); - expect(columnSymbol.references.length).toBe(1); - expect(columnSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(columnSymbol.references[0].referee).toBe(columnSymbol); + const colRefs = compiler.symbolReferences(columnSymbol).getValue(); + expect(colRefs.length).toBe(1); + expect(colRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(colRefs[0]).getValue()).toBe(columnSymbol); }); test('should detect unknown table and column references', () => { - const errors1 = analyze('Ref: nonexistent.id > also_nonexistent.id').getErrors(); + const { errors: errors1 } = createCompilerAndAnalyze('Ref: nonexistent.id > also_nonexistent.id'); expect(errors1).toHaveLength(2); expect(errors1[0].diagnostic).toBe("Table 'nonexistent' does not exist in Schema 'public'"); expect(errors1[1].diagnostic).toBe("Table 'also_nonexistent' does not exist in Schema 'public'"); @@ -716,7 +761,7 @@ describe('[example] binder', () => { Table users { id int } Table posts { user_id int [ref: > users.nonexistent] } `; - const errors2 = analyze(source2).getErrors(); + const { errors: errors2 } = createCompilerAndAnalyze(source2); expect(errors2).toHaveLength(1); expect(errors2[0].diagnostic).toBe("Column 'nonexistent' does not exist in Table 'users'"); }); @@ -727,17 +772,17 @@ describe('[example] binder', () => { Table public.posts { user_id int [ref: > auth.users.id] } Ref: auth.users.id < auth.users.id `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); - const usersSymbol = elements[0].symbol as TableSymbol; + const usersSymbol = compiler.nodeSymbol(elements[0]).getValue(); - expect(usersSymbol.references.length).toBe(3); - usersSymbol.references.forEach((refNode) => { + const refs = compiler.symbolReferences(usersSymbol).getValue(); + expect(refs.length).toBe(3); + refs.forEach((refNode) => { expect(refNode.kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(refNode.referee).toBe(usersSymbol); + expect(compiler.nodeReferee(refNode).getValue()).toBe(usersSymbol); }); }); @@ -750,22 +795,23 @@ describe('[example] binder', () => { post_id int [ref: > posts.id] } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); - const usersSymbol = schemaSymbol.symbolTable.get('Table:users') as TableSymbol; - const postsSymbol = schemaSymbol.symbolTable.get('Table:posts') as TableSymbol; + const usersSymbol = findMember(compiler, programSymbol, 'users', SymbolKind.Table)!; + const postsSymbol = findMember(compiler, programSymbol, 'posts', SymbolKind.Table)!; - expect(usersSymbol.references.length).toBe(1); - expect(usersSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(usersSymbol.references[0].referee).toBe(usersSymbol); + const usersRefs = compiler.symbolReferences(usersSymbol).getValue(); + expect(usersRefs.length).toBe(1); + expect(usersRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(usersRefs[0]).getValue()).toBe(usersSymbol); - expect(postsSymbol.references.length).toBe(1); - expect(postsSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(postsSymbol.references[0].referee).toBe(postsSymbol); + const postsRefs = compiler.symbolReferences(postsSymbol).getValue(); + expect(postsRefs.length).toBe(1); + expect(postsRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(postsRefs[0]).getValue()).toBe(postsSymbol); }); test('should allow forward reference to table', () => { @@ -774,13 +820,12 @@ describe('[example] binder', () => { Table users { id int } Table posts { user_id int } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('Table:users')).toBeInstanceOf(TableSymbol); - expect(schemaSymbol.symbolTable.get('Table:posts')).toBeInstanceOf(TableSymbol); + const programSymbol = compiler.nodeSymbol(ast).getValue(); + expect(findMember(compiler, programSymbol, 'users', SymbolKind.Table)).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); + expect(findMember(compiler, programSymbol, 'posts', SymbolKind.Table)).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); }); test('should track multiple references to the same symbol', () => { @@ -789,14 +834,18 @@ describe('[example] binder', () => { Ref r1: users.id < users.id Ref r2: users.id < users.id `; - const ast = analyze(source).getValue(); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); - const usersSymbol = elements[0].symbol as TableSymbol; + const usersSymbol = compiler.nodeSymbol(elements[0]).getValue(); - expect(usersSymbol.references.length).toBe(4); - usersSymbol.references.forEach((refNode) => { + const refs = compiler.symbolReferences(usersSymbol).getValue(); + expect(refs.length).toBe(4); + refs.forEach((refNode) => { expect(refNode.kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(refNode.referee).toBe(usersSymbol); + expect(compiler.nodeReferee(refNode).getValue()).toBe(usersSymbol); }); }); @@ -812,32 +861,32 @@ describe('[example] binder', () => { } Ref: orders.(merchant_id, country) > merchants.(id, country_code) `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const merchantsSymbol = schemaSymbol.symbolTable.get('Table:merchants') as TableSymbol; - const ordersSymbol = schemaSymbol.symbolTable.get('Table:orders') as TableSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const merchantsSymbol = findMember(compiler, programSymbol, 'merchants', SymbolKind.Table)!; + const ordersSymbol = findMember(compiler, programSymbol, 'orders', SymbolKind.Table)!; // Both tables should have 2 references (table name + tuple access) - expect(merchantsSymbol.references.length).toBe(2); - expect(ordersSymbol.references.length).toBe(2); + expect(compiler.symbolReferences(merchantsSymbol).getValue().length).toBe(2); + expect(compiler.symbolReferences(ordersSymbol).getValue().length).toBe(2); // Check column references - const idColumn = merchantsSymbol.symbolTable.get('Column:id') as ColumnSymbol; - const countryCodeColumn = merchantsSymbol.symbolTable.get('Column:country_code') as ColumnSymbol; - const merchantIdColumn = ordersSymbol.symbolTable.get('Column:merchant_id') as ColumnSymbol; - const countryColumn = ordersSymbol.symbolTable.get('Column:country') as ColumnSymbol; + const idColumn = findMember(compiler, merchantsSymbol, 'id', SymbolKind.Column)!; + const countryCodeColumn = findMember(compiler, merchantsSymbol, 'country_code', SymbolKind.Column)!; + const merchantIdColumn = findMember(compiler, ordersSymbol, 'merchant_id', SymbolKind.Column)!; + const countryColumn = findMember(compiler, ordersSymbol, 'country', SymbolKind.Column)!; - expect(idColumn.references.length).toBe(1); - expect(countryCodeColumn.references.length).toBe(1); - expect(merchantIdColumn.references.length).toBe(1); - expect(countryColumn.references.length).toBe(1); + expect(compiler.symbolReferences(idColumn).getValue().length).toBe(1); + expect(compiler.symbolReferences(countryCodeColumn).getValue().length).toBe(1); + expect(compiler.symbolReferences(merchantIdColumn).getValue().length).toBe(1); + expect(compiler.symbolReferences(countryColumn).getValue().length).toBe(1); // Verify all references have correct referee [idColumn, countryCodeColumn, merchantIdColumn, countryColumn].forEach((col) => { - expect(col.references[0].referee).toBe(col); + const colRefs = compiler.symbolReferences(col).getValue(); + expect(compiler.nodeReferee(colRefs[0]).getValue()).toBe(col); }); }); @@ -853,17 +902,16 @@ describe('[example] binder', () => { } Ref: shop.orders.(product_id, category) > shop.products.(id, category_id) `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const publicSchema = ast.symbol as SchemaSymbol; - const shopSchema = publicSchema.symbolTable.get('Schema:shop') as SchemaSymbol; - const productsSymbol = shopSchema.symbolTable.get('Table:products') as TableSymbol; - const ordersSymbol = shopSchema.symbolTable.get('Table:orders') as TableSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const shopSchema = findMember(compiler, programSymbol, 'shop', SymbolKind.Schema)!; + const productsSymbol = findMember(compiler, shopSchema, 'products', SymbolKind.Table)!; + const ordersSymbol = findMember(compiler, shopSchema, 'orders', SymbolKind.Table)!; - expect(productsSymbol.references.length).toBe(2); - expect(ordersSymbol.references.length).toBe(2); + expect(compiler.symbolReferences(productsSymbol).getValue().length).toBe(2); + expect(compiler.symbolReferences(ordersSymbol).getValue().length).toBe(2); }); test('should detect errors in composite foreign key references', () => { @@ -872,7 +920,7 @@ describe('[example] binder', () => { Table posts { user_id int } Ref: posts.(user_id, nonexistent) > users.(id, also_nonexistent) `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(2); expect(errors[0].diagnostic).toBe("Column 'nonexistent' does not exist in Table 'posts'"); expect(errors[1].diagnostic).toBe("Column 'also_nonexistent' does not exist in Table 'users'"); @@ -881,19 +929,23 @@ describe('[example] binder', () => { describe('TablePartial', () => { test('should create TablePartialSymbol with correct properties', () => { - const ast = analyze('TablePartial timestamps { created_at timestamp }').getValue(); + const compiler = new Compiler(); + compiler.setSource('TablePartial timestamps { created_at timestamp }'); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); const partialNode = elements[0]; - const partialSymbol = partialNode.symbol as TablePartialSymbol; + const partialSymbol = compiler.nodeSymbol(partialNode).getValue(); - expect(partialSymbol).toBeInstanceOf(TablePartialSymbol); + expect(partialSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.TablePartial)); expect(partialSymbol.declaration).toBe(partialNode); - expect(partialSymbol.symbolTable.get('Column:created_at')).toBeInstanceOf(ColumnSymbol); - expect(partialSymbol.references).toEqual([]); + expect(findMember(compiler, partialSymbol, 'created_at', SymbolKind.Column)).toSatisfy((s: any) => s.isKind(SymbolKind.Column)); + expect(compiler.symbolReferences(partialSymbol).getValue()).toEqual([]); - // Verify TablePartial is in public schema symbol table - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('TablePartial:timestamps')).toBe(partialSymbol); + // Verify TablePartial is in program symbol members + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const timestampsPartial = findMember(compiler, programSymbol, 'timestamps', SymbolKind.TablePartial); + expect(timestampsPartial).toBe(partialSymbol); }); test('should bind TablePartial references and track injections', () => { @@ -904,17 +956,17 @@ describe('[example] binder', () => { ~timestamps } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); const partial = elements.find((e) => e.type?.value === 'TablePartial'); - const partialSymbol = partial?.symbol as TablePartialSymbol; + const partialSymbol = compiler.nodeSymbol(partial!).getValue(); - expect(partialSymbol.references.length).toBe(1); - expect(partialSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(partialSymbol.references[0].referee).toBe(partialSymbol); + const refs = compiler.symbolReferences(partialSymbol).getValue(); + expect(refs.length).toBe(1); + expect(refs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(refs[0]).getValue()).toBe(partialSymbol); }); test('should detect unknown TablePartial references', () => { @@ -924,7 +976,7 @@ describe('[example] binder', () => { ~nonexistent_partial } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors).toHaveLength(1); expect(errors[0].diagnostic).toBe("TablePartial 'nonexistent_partial' does not exist in Schema 'public'"); @@ -940,22 +992,23 @@ describe('[example] binder', () => { ~audit } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); - const timestampsSymbol = schemaSymbol.symbolTable.get('TablePartial:timestamps') as TablePartialSymbol; - const auditSymbol = schemaSymbol.symbolTable.get('TablePartial:audit') as TablePartialSymbol; + const timestampsSymbol = findMember(compiler, programSymbol, 'timestamps', SymbolKind.TablePartial)!; + const auditSymbol = findMember(compiler, programSymbol, 'audit', SymbolKind.TablePartial)!; - expect(timestampsSymbol.references.length).toBe(1); - expect(timestampsSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(timestampsSymbol.references[0].referee).toBe(timestampsSymbol); + const tsRefs = compiler.symbolReferences(timestampsSymbol).getValue(); + expect(tsRefs.length).toBe(1); + expect(tsRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(tsRefs[0]).getValue()).toBe(timestampsSymbol); - expect(auditSymbol.references.length).toBe(1); - expect(auditSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(auditSymbol.references[0].referee).toBe(auditSymbol); + const auditRefs = compiler.symbolReferences(auditSymbol).getValue(); + expect(auditRefs.length).toBe(1); + expect(auditRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(auditRefs[0]).getValue()).toBe(auditSymbol); }); test('should handle tables with only partial injections', () => { @@ -963,18 +1016,18 @@ describe('[example] binder', () => { TablePartial base { id int } Table derived { ~base } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); - - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const baseSymbol = schemaSymbol.symbolTable.get('TablePartial:base') as TablePartialSymbol; - const derivedSymbol = schemaSymbol.symbolTable.get('Table:derived') as TableSymbol; - - expect(baseSymbol.references.length).toBe(1); - expect(baseSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(baseSymbol.references[0].referee).toBe(baseSymbol); - expect(derivedSymbol).toBeInstanceOf(TableSymbol); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); + + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const baseSymbol = findMember(compiler, programSymbol, 'base', SymbolKind.TablePartial)!; + const derivedSymbol = findMember(compiler, programSymbol, 'derived', SymbolKind.Table)!; + + const baseRefs = compiler.symbolReferences(baseSymbol).getValue(); + expect(baseRefs.length).toBe(1); + expect(baseRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(baseRefs[0]).getValue()).toBe(baseSymbol); + expect(derivedSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); }); test('should allow forward reference to TablePartial', () => { @@ -985,13 +1038,12 @@ describe('[example] binder', () => { } TablePartial timestamps { created_at timestamp } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('Table:users')).toBeInstanceOf(TableSymbol); - expect(schemaSymbol.symbolTable.get('TablePartial:timestamps')).toBeInstanceOf(TablePartialSymbol); + const programSymbol = compiler.nodeSymbol(ast).getValue(); + expect(findMember(compiler, programSymbol, 'users', SymbolKind.Table)).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); + expect(findMember(compiler, programSymbol, 'timestamps', SymbolKind.TablePartial)).toSatisfy((s: any) => s.isKind(SymbolKind.TablePartial)); }); test('should detect non-existent TablePartial injection', () => { @@ -1003,7 +1055,7 @@ describe('[example] binder', () => { ~p2 } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toEqual(1); expect(errors[0].diagnostic).toBe("TablePartial 'p2' does not exist in Schema 'public'"); }); @@ -1020,7 +1072,7 @@ describe('[example] binder', () => { col type [ref: > un_col] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(4); const errorDiagnostics = errors.map((e) => e.diagnostic); @@ -1036,7 +1088,7 @@ describe('[example] binder', () => { col type [ref: > col] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); // Self-referential refs in table partials are allowed expect(errors.length).toBe(0); }); @@ -1053,7 +1105,7 @@ describe('[example] binder', () => { col2 type [ref: > col3] } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); // Circular refs via table partials are allowed expect(errors.length).toBe(0); }); @@ -1073,17 +1125,17 @@ describe('[example] binder', () => { ~common } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const usersSymbol = schemaSymbol.symbolTable.get('Table:users') as TableSymbol; - const idColumn = usersSymbol.symbolTable.get('Column:id') as ColumnSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const usersSymbol = findMember(compiler, programSymbol, 'users', SymbolKind.Table)!; + const idColumn = findMember(compiler, usersSymbol, 'id', SymbolKind.Column)!; // users.id should be referenced from the partial's inline ref - expect(idColumn.references.length).toBe(1); - expect(idColumn.references[0].referee).toBe(idColumn); + const idRefs = compiler.symbolReferences(idColumn).getValue(); + expect(idRefs.length).toBe(1); + expect(compiler.nodeReferee(idRefs[0]).getValue()).toBe(idColumn); }); }); @@ -1095,19 +1147,22 @@ describe('[example] binder', () => { users } `; - const ast = analyze(source).getValue(); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + compiler.bind(ast); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); const tableGroup = elements.find((e) => e.type?.value === 'TableGroup'); - const groupSymbol = tableGroup?.symbol as TableGroupSymbol; + const groupSymbol = compiler.nodeSymbol(tableGroup!).getValue(); - expect(groupSymbol).toBeInstanceOf(TableGroupSymbol); + expect(groupSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.TableGroup)); expect(groupSymbol.declaration).toBe(tableGroup); - expect(groupSymbol.symbolTable).toBeDefined(); - expect(groupSymbol.references).toEqual([]); + expect(compiler.symbolReferences(groupSymbol).getValue()).toEqual([]); - // Verify TableGroup is in public schema symbol table - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('TableGroup:group1')).toBe(groupSymbol); + // Verify TableGroup is in program symbol members + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const group1 = findMember(compiler, programSymbol, 'group1', SymbolKind.TableGroup); + expect(group1).toBe(groupSymbol); }); test('should bind table references and track them', () => { @@ -1119,21 +1174,22 @@ describe('[example] binder', () => { posts } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); const elements = ast.body.filter((n): n is ElementDeclarationNode => n.kind === SyntaxNodeKind.ELEMENT_DECLARATION); - const usersSymbol = elements[0].symbol as TableSymbol; - const postsSymbol = elements[1].symbol as TableSymbol; - - expect(usersSymbol.references.length).toBe(1); - expect(usersSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(usersSymbol.references[0].referee).toBe(usersSymbol); - - expect(postsSymbol.references.length).toBe(1); - expect(postsSymbol.references[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); - expect(postsSymbol.references[0].referee).toBe(postsSymbol); + const usersSymbol = compiler.nodeSymbol(elements[0]).getValue(); + const postsSymbol = compiler.nodeSymbol(elements[1]).getValue(); + + const usersRefs = compiler.symbolReferences(usersSymbol).getValue(); + expect(usersRefs.length).toBe(1); + expect(usersRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(usersRefs[0]).getValue()).toBe(usersSymbol); + + const postsRefs = compiler.symbolReferences(postsSymbol).getValue(); + expect(postsRefs.length).toBe(1); + expect(postsRefs[0].kind).toBe(SyntaxNodeKind.PRIMARY_EXPRESSION); + expect(compiler.nodeReferee(postsRefs[0]).getValue()).toBe(postsSymbol); }); }); @@ -1145,12 +1201,11 @@ describe('[example] binder', () => { } Table users { id int } `; - const result = analyze(source); - expect(result.getErrors()).toHaveLength(0); + const { compiler, ast, errors } = createCompilerAndAnalyze(source); + expect(errors).toHaveLength(0); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - expect(schemaSymbol.symbolTable.get('Table:users')).toBeInstanceOf(TableSymbol); + const programSymbol = compiler.nodeSymbol(ast).getValue(); + expect(findMember(compiler, programSymbol, 'users', SymbolKind.Table)).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); }); }); }); diff --git a/packages/dbml-parse/__tests__/examples/binder/records.test.ts b/packages/dbml-parse/__tests__/examples/binder/records.test.ts index 3e109a538..238fff4d9 100644 --- a/packages/dbml-parse/__tests__/examples/binder/records.test.ts +++ b/packages/dbml-parse/__tests__/examples/binder/records.test.ts @@ -1,6 +1,30 @@ import { describe, expect, test } from 'vitest'; -import { TableSymbol, EnumSymbol, ColumnSymbol, EnumFieldSymbol, SchemaSymbol } from '@/core/analyzer/symbol/symbols'; -import { analyze } from '@tests/utils'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { UNHANDLED } from '@/constants'; +import { Compiler } from '@/index'; + +function findMember (compiler: Compiler, parentSymbol: NodeSymbol, name: string, kind: SymbolKind): NodeSymbol | undefined { + const members = compiler.symbolMembers(parentSymbol); + if (members.hasValue(UNHANDLED)) return undefined; + return members.getValue().find((m) => { + if (!m.isKind(kind)) return false; + if (!m.declaration) return false; + const fn = compiler.fullname(m.declaration); + if (fn.hasValue(UNHANDLED)) return false; + return fn.getValue()?.at(-1) === name; + }); +} + +function createCompilerAndAnalyze (source: string): { compiler: Compiler; ast: any; errors: any[] } { + const compiler = new Compiler(); + compiler.setSource(source); + const parseResult = compiler.parseFile(); + const { ast } = parseResult.getValue(); + const bindResult = compiler.bind(ast); + const validateResult = compiler.validate(ast); + const errors = [...parseResult.getErrors(), ...bindResult.getErrors(), ...validateResult.getErrors()]; + return { compiler, ast, errors }; +} describe('[example] records binder', () => { test('should bind records to table and columns', () => { @@ -14,26 +38,31 @@ describe('[example] records binder', () => { 2, "Bob" } `; - const result = analyze(source); - expect(result.getErrors().length).toBe(0); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + expect(compiler.parseFile().getErrors().length).toBe(0); + compiler.bind(ast); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const tableSymbol = schemaSymbol.symbolTable.get('Table:users') as TableSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const tableSymbol = findMember(compiler, programSymbol, 'users', SymbolKind.Table)!; // Table should have exactly 1 reference from records - expect(tableSymbol.references.length).toBe(1); - expect(tableSymbol.references[0].referee).toBe(tableSymbol); + const tableRefs = compiler.symbolReferences(tableSymbol).getValue(); + expect(tableRefs.length).toBe(1); + expect(compiler.nodeReferee(tableRefs[0]).getValue()).toBe(tableSymbol); - const idColumn = tableSymbol.symbolTable.get('Column:id') as ColumnSymbol; - const nameColumn = tableSymbol.symbolTable.get('Column:name') as ColumnSymbol; + const idColumn = findMember(compiler, tableSymbol, 'id', SymbolKind.Column)!; + const nameColumn = findMember(compiler, tableSymbol, 'name', SymbolKind.Column)!; // Each column should have exactly 1 reference from records column list - expect(idColumn.references.length).toBe(1); - expect(idColumn.references[0].referee).toBe(idColumn); + const idRefs = compiler.symbolReferences(idColumn).getValue(); + expect(idRefs.length).toBe(1); + expect(compiler.nodeReferee(idRefs[0]).getValue()).toBe(idColumn); - expect(nameColumn.references.length).toBe(1); - expect(nameColumn.references[0].referee).toBe(nameColumn); + const nameRefs = compiler.symbolReferences(nameColumn).getValue(); + expect(nameRefs.length).toBe(1); + expect(compiler.nodeReferee(nameRefs[0]).getValue()).toBe(nameColumn); }); test('should bind records with schema-qualified table', () => { @@ -46,29 +75,32 @@ describe('[example] records binder', () => { 1, "alice@example.com" } `; - const result = analyze(source); - expect(result.getErrors().length).toBe(0); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + expect(compiler.parseFile().getErrors().length).toBe(0); + compiler.bind(ast); - const ast = result.getValue(); - const publicSchema = ast.symbol as SchemaSymbol; - const authSchema = publicSchema.symbolTable.get('Schema:auth') as SchemaSymbol; - const tableSymbol = authSchema.symbolTable.get('Table:users') as TableSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const authSchema = findMember(compiler, programSymbol, 'auth', SymbolKind.Schema)!; + const tableSymbol = findMember(compiler, authSchema, 'users', SymbolKind.Table)!; // Schema should have reference from records - expect(authSchema.references.length).toBe(1); - expect(authSchema.references[0].referee).toBe(authSchema); - + const authSchemaRefs = compiler.symbolReferences(authSchema).getValue(); + expect(authSchemaRefs.length).toBe(1); + expect(compiler.nodeReferee(authSchemaRefs[0]).getValue()).toBe(authSchema); // Table should have exactly 1 reference from records - expect(tableSymbol.references.length).toBe(1); - expect(tableSymbol.references[0].referee).toBe(tableSymbol); + const tableRefs = compiler.symbolReferences(tableSymbol).getValue(); + expect(tableRefs.length).toBe(1); + expect(compiler.nodeReferee(tableRefs[0]).getValue()).toBe(tableSymbol); // Columns should have references - const idColumn = tableSymbol.symbolTable.get('Column:id') as ColumnSymbol; - const emailColumn = tableSymbol.symbolTable.get('Column:email') as ColumnSymbol; + const idColumn = findMember(compiler, tableSymbol, 'id', SymbolKind.Column)!; + const emailColumn = findMember(compiler, tableSymbol, 'email', SymbolKind.Column)!; - expect(idColumn.references.length).toBe(1); + expect(compiler.symbolReferences(idColumn).getValue().length).toBe(1); - expect(emailColumn.references.length).toBe(1); + expect(compiler.symbolReferences(emailColumn).getValue().length).toBe(1); }); test('should detect unknown table in records', () => { @@ -77,7 +109,7 @@ describe('[example] records binder', () => { 1 } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Table 'nonexistent' does not exist in Schema 'public'"); }); @@ -91,7 +123,7 @@ describe('[example] records binder', () => { 1, "value" } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Column 'nonexistent' does not exist in Table 'users'"); }); @@ -109,23 +141,25 @@ describe('[example] records binder', () => { 2, "Bob" } `; - const result = analyze(source); - expect(result.getErrors().length).toBe(0); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + expect(compiler.parseFile().getErrors().length).toBe(0); + compiler.bind(ast); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const tableSymbol = schemaSymbol.symbolTable.get('Table:users') as TableSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const tableSymbol = findMember(compiler, programSymbol, 'users', SymbolKind.Table)!; // Table should have exactly 2 references from both records elements - expect(tableSymbol.references.length).toBe(2); + expect(compiler.symbolReferences(tableSymbol).getValue().length).toBe(2); // Each column should have exactly 2 references - const idColumn = tableSymbol.symbolTable.get('Column:id') as ColumnSymbol; - const nameColumn = tableSymbol.symbolTable.get('Column:name') as ColumnSymbol; + const idColumn = findMember(compiler, tableSymbol, 'id', SymbolKind.Column)!; + const nameColumn = findMember(compiler, tableSymbol, 'name', SymbolKind.Column)!; - expect(idColumn.references.length).toBe(2); + expect(compiler.symbolReferences(idColumn).getValue().length).toBe(2); - expect(nameColumn.references.length).toBe(2); + expect(compiler.symbolReferences(nameColumn).getValue().length).toBe(2); }); test('should bind records with enum column type', () => { @@ -139,20 +173,23 @@ describe('[example] records binder', () => { 1, status.active } `; - const result = analyze(source); - expect(result.getErrors().length).toBe(0); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + expect(compiler.parseFile().getErrors().length).toBe(0); + compiler.bind(ast); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const enumSymbol = schemaSymbol.symbolTable.get('Enum:status') as EnumSymbol; - const activeField = enumSymbol.symbolTable.get('Enum field:active') as EnumFieldSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const enumSymbol = findMember(compiler, programSymbol, 'status', SymbolKind.Enum)!; + const activeField = findMember(compiler, enumSymbol, 'active', SymbolKind.EnumField)!; // Enum should have 2 references: 1 from column type, 1 from records data - expect(enumSymbol.references.length).toBe(2); + expect(compiler.symbolReferences(enumSymbol).getValue().length).toBe(2); // Enum field should have exactly 1 reference from records value - expect(activeField.references.length).toBe(1); - expect(activeField.references[0].referee).toBe(activeField); + const activeRefs = compiler.symbolReferences(activeField).getValue(); + expect(activeRefs.length).toBe(1); + expect(compiler.nodeReferee(activeRefs[0]).getValue()).toBe(activeField); }); test('should allow forward reference to table in records', () => { @@ -165,21 +202,23 @@ describe('[example] records binder', () => { name varchar } `; - const result = analyze(source); - expect(result.getErrors().length).toBe(0); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + expect(compiler.parseFile().getErrors().length).toBe(0); + compiler.bind(ast); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const tableSymbol = schemaSymbol.symbolTable.get('Table:users') as TableSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const tableSymbol = findMember(compiler, programSymbol, 'users', SymbolKind.Table)!; // Verify forward reference is properly bound - expect(tableSymbol.references.length).toBe(1); + expect(compiler.symbolReferences(tableSymbol).getValue().length).toBe(1); - const idColumn = tableSymbol.symbolTable.get('Column:id') as ColumnSymbol; - const nameColumn = tableSymbol.symbolTable.get('Column:name') as ColumnSymbol; + const idColumn = findMember(compiler, tableSymbol, 'id', SymbolKind.Column)!; + const nameColumn = findMember(compiler, tableSymbol, 'name', SymbolKind.Column)!; - expect(idColumn.references.length).toBe(1); - expect(nameColumn.references.length).toBe(1); + expect(compiler.symbolReferences(idColumn).getValue().length).toBe(1); + expect(compiler.symbolReferences(nameColumn).getValue().length).toBe(1); }); test('should bind schema-qualified enum values in records', () => { @@ -194,25 +233,29 @@ describe('[example] records binder', () => { 2, auth.role.user } `; - const result = analyze(source); - expect(result.getErrors().length).toBe(0); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + expect(compiler.parseFile().getErrors().length).toBe(0); + compiler.bind(ast); - const ast = result.getValue(); - const publicSchema = ast.symbol as SchemaSymbol; - const authSchema = publicSchema.symbolTable.get('Schema:auth') as SchemaSymbol; - const enumSymbol = authSchema.symbolTable.get('Enum:role') as EnumSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const authSchema = findMember(compiler, programSymbol, 'auth', SymbolKind.Schema)!; + const enumSymbol = findMember(compiler, authSchema, 'role', SymbolKind.Enum)!; // Enum should have 3 references: 1 from column type, 2 from records data - expect(enumSymbol.references.length).toBe(3); + expect(compiler.symbolReferences(enumSymbol).getValue().length).toBe(3); - const adminField = enumSymbol.symbolTable.get('Enum field:admin') as EnumFieldSymbol; - const userField = enumSymbol.symbolTable.get('Enum field:user') as EnumFieldSymbol; + const adminField = findMember(compiler, enumSymbol, 'admin', SymbolKind.EnumField)!; + const userField = findMember(compiler, enumSymbol, 'user', SymbolKind.EnumField)!; - expect(adminField.references.length).toBe(1); - expect(adminField.references[0].referee).toBe(adminField); + const adminRefs = compiler.symbolReferences(adminField).getValue(); + expect(adminRefs.length).toBe(1); + expect(compiler.nodeReferee(adminRefs[0]).getValue()).toBe(adminField); - expect(userField.references.length).toBe(1); - expect(userField.references[0].referee).toBe(userField); + const userRefs = compiler.symbolReferences(userField).getValue(); + expect(userRefs.length).toBe(1); + expect(compiler.nodeReferee(userRefs[0]).getValue()).toBe(userField); }); test('should detect unknown enum in records data', () => { @@ -225,7 +268,7 @@ describe('[example] records binder', () => { 1, unknown_enum.value } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Enum 'unknown_enum' does not exist in Schema 'public'"); }); @@ -241,7 +284,7 @@ describe('[example] records binder', () => { 1, status.unknown_field } `; - const errors = analyze(source).getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(1); expect(errors[0].diagnostic).toBe("Enum field 'unknown_field' does not exist in Enum 'status'"); }); @@ -260,25 +303,27 @@ describe('[example] records binder', () => { 4, status.pending } `; - const result = analyze(source); - expect(result.getErrors().length).toBe(0); + const compiler = new Compiler(); + compiler.setSource(source); + const { ast } = compiler.parseFile().getValue(); + expect(compiler.parseFile().getErrors().length).toBe(0); + compiler.bind(ast); - const ast = result.getValue(); - const schemaSymbol = ast.symbol as SchemaSymbol; - const enumSymbol = schemaSymbol.symbolTable.get('Enum:status') as EnumSymbol; + const programSymbol = compiler.nodeSymbol(ast).getValue(); + const enumSymbol = findMember(compiler, programSymbol, 'status', SymbolKind.Enum)!; - const pendingField = enumSymbol.symbolTable.get('Enum field:pending') as EnumFieldSymbol; - const activeField = enumSymbol.symbolTable.get('Enum field:active') as EnumFieldSymbol; - const completedField = enumSymbol.symbolTable.get('Enum field:completed') as EnumFieldSymbol; + const pendingField = findMember(compiler, enumSymbol, 'pending', SymbolKind.EnumField)!; + const activeField = findMember(compiler, enumSymbol, 'active', SymbolKind.EnumField)!; + const completedField = findMember(compiler, enumSymbol, 'completed', SymbolKind.EnumField)!; // pending is referenced twice - expect(pendingField.references.length).toBe(2); + expect(compiler.symbolReferences(pendingField).getValue().length).toBe(2); // active is referenced once - expect(activeField.references.length).toBe(1); + expect(compiler.symbolReferences(activeField).getValue().length).toBe(1); // completed is referenced once - expect(completedField.references.length).toBe(1); + expect(compiler.symbolReferences(completedField).getValue().length).toBe(1); }); test('should error when there are duplicate columns in top-level records', () => { @@ -294,8 +339,7 @@ describe('[example] records binder', () => { 4, 40 } `; - const result = analyze(source); - const errors = result.getErrors(); + const { errors } = createCompilerAndAnalyze(source); expect(errors.length).toBe(4); expect(errors[0].message).toBe('Column \'id\' is referenced more than once in a Records for Table \'tasks\''); expect(errors[1].message).toBe('Column \'id\' is referenced more than once in a Records for Table \'tasks\''); diff --git a/packages/dbml-parse/__tests__/examples/compiler/applyTextEdits.test.ts b/packages/dbml-parse/__tests__/examples/compiler/applyTextEdits.test.ts index 33df75ca0..cebde3c95 100644 --- a/packages/dbml-parse/__tests__/examples/compiler/applyTextEdits.test.ts +++ b/packages/dbml-parse/__tests__/examples/compiler/applyTextEdits.test.ts @@ -222,7 +222,7 @@ describe('[example] applyTextEdits', () => { const compiler = new Compiler(); compiler.setSource('Table users { id int }'); - const result = compiler.applyTextEdits([ + const result = applyTextEdits(compiler.parse.source(), [ { start: 6, end: 11, newText: 'customers' }, ]); @@ -236,7 +236,7 @@ describe('[example] applyTextEdits', () => { email varchar }`); - const result = compiler.applyTextEdits([ + const result = applyTextEdits(compiler.parse.source(), [ { start: 6, end: 11, newText: 'customers' }, { start: 30, end: 35, newText: 'name' }, ]); @@ -250,7 +250,7 @@ describe('[example] applyTextEdits', () => { const compiler = new Compiler(); compiler.setSource(originalSource); - compiler.applyTextEdits([ + applyTextEdits(compiler.parse.source(), [ { start: 6, end: 11, newText: 'customers' }, ]); diff --git a/packages/dbml-parse/__tests__/examples/lexer/lexer.test.ts b/packages/dbml-parse/__tests__/examples/lexer/lexer.test.ts index bacd09bf3..6c328231c 100644 --- a/packages/dbml-parse/__tests__/examples/lexer/lexer.test.ts +++ b/packages/dbml-parse/__tests__/examples/lexer/lexer.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { SyntaxTokenKind, SyntaxToken, isTriviaToken } from '@/core/lexer/tokens'; -import { CompileErrorCode } from '@/core/errors'; +import { SyntaxTokenKind, SyntaxToken, isTriviaToken } from '@/core/types/tokens'; +import { CompileErrorCode } from '@/core/types/errors'; import { lex } from '@tests/utils'; // Helper to get non-trivia, non-EOF tokens diff --git a/packages/dbml-parse/__tests__/examples/lexer/scientific-notation.test.ts b/packages/dbml-parse/__tests__/examples/lexer/scientific-notation.test.ts index 680ba8f18..e093869c2 100644 --- a/packages/dbml-parse/__tests__/examples/lexer/scientific-notation.test.ts +++ b/packages/dbml-parse/__tests__/examples/lexer/scientific-notation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { SyntaxTokenKind, isTriviaToken } from '@/core/lexer/tokens'; -import { CompileErrorCode } from '@/core/errors'; +import { SyntaxTokenKind, isTriviaToken } from '@/core/types/tokens'; +import { CompileErrorCode } from '@/core/types/errors'; import { lex } from '@tests/utils'; // Helper to get non-trivia, non-EOF tokens diff --git a/packages/dbml-parse/__tests__/examples/parser/parser.test.ts b/packages/dbml-parse/__tests__/examples/parser/parser.test.ts index 557ab5e0e..521a97dc4 100644 --- a/packages/dbml-parse/__tests__/examples/parser/parser.test.ts +++ b/packages/dbml-parse/__tests__/examples/parser/parser.test.ts @@ -14,8 +14,8 @@ import { VariableNode, CommaExpressionNode, LiteralNode, -} from '@/core/parser/nodes'; -import { SyntaxTokenKind } from '@/core/lexer/tokens'; +} from '@/core/types/nodes'; +import { SyntaxTokenKind } from '@/core/types/tokens'; import { parse } from '@tests/utils'; // Helper to extract a value from a PrimaryExpressionNode diff --git a/packages/dbml-parse/__tests__/examples/services/definition/general.test.ts b/packages/dbml-parse/__tests__/examples/services/definition/general.test.ts index 901e0f34c..b649d2d23 100644 --- a/packages/dbml-parse/__tests__/examples/services/definition/general.test.ts +++ b/packages/dbml-parse/__tests__/examples/services/definition/general.test.ts @@ -630,19 +630,7 @@ Ref: ecommerce.orders.user_id > ecommerce.users.id`; const position = createPosition(9, 50); const definitions = definitionProvider.provideDefinition(model, position); - expect(definitions).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 12, - "endLineNumber": 2, - "startColumn": 3, - "startLineNumber": 2, - }, - "uri": "", - }, - ] - `); + expect(definitions).toMatchInlineSnapshot(`[]`); }); it('- should find schema-qualified enum definition', () => { @@ -818,7 +806,19 @@ TableGroup group1 { const position = createPosition(7, 21); const definitions = definitionProvider.provideDefinition(model, position); - expect(definitions).toMatchInlineSnapshot('[]'); + expect(definitions).toMatchInlineSnapshot(` + [ + { + "range": { + "endColumn": 17, + "endLineNumber": 4, + "startColumn": 3, + "startLineNumber": 4, + }, + "uri": "", + }, + ] + `); }); it('- should find column in named index', () => { @@ -928,10 +928,10 @@ Ref: users.created_at > logs.timestamp`; [ { "range": { - "endColumn": 19, - "endLineNumber": 8, + "endColumn": 23, + "endLineNumber": 2, "startColumn": 3, - "startLineNumber": 8, + "startLineNumber": 2, }, "uri": "", }, @@ -1317,19 +1317,7 @@ Ref: orders.user_id > myproject.ecommerce.users.id`; const position = createPosition(9, 44); const definitions = definitionProvider.provideDefinition(model, position); - expect(definitions).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 2, - "endLineNumber": 3, - "startColumn": 1, - "startLineNumber": 1, - }, - "uri": "", - }, - ] - `); + expect(definitions).toMatchInlineSnapshot(`[]`); expect(Array.isArray(definitions)).toBeTruthy(); if (!Array.isArray(definitions)) return; @@ -1635,19 +1623,7 @@ Ref: posts.(author_first, author_last) > users.(first_name, last_name)`; const position = createPosition(11, 57); const definitions = definitionProvider.provideDefinition(model, position); - expect(definitions).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 21, - "endLineNumber": 2, - "startColumn": 3, - "startLineNumber": 2, - }, - "uri": "", - }, - ] - `); + expect(definitions).toMatchInlineSnapshot(`[]`); expect(Array.isArray(definitions)).toBeTruthy(); if (!Array.isArray(definitions)) return; @@ -2849,19 +2825,7 @@ Records public.orders(id, customer_name) { const position = createPosition(7, 17); const definitions = definitionProvider.provideDefinition(model, position); - expect(definitions).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 2, - "endLineNumber": 5, - "startColumn": 1, - "startLineNumber": 1, - }, - "uri": "", - }, - ] - `); + expect(definitions).toMatchInlineSnapshot(`[]`); }); it('- should find enum definition from Records data', () => { diff --git a/packages/dbml-parse/__tests__/examples/services/references/general.test.ts b/packages/dbml-parse/__tests__/examples/services/references/general.test.ts index 0390c2967..27c2138ab 100644 --- a/packages/dbml-parse/__tests__/examples/services/references/general.test.ts +++ b/packages/dbml-parse/__tests__/examples/services/references/general.test.ts @@ -40,19 +40,7 @@ Ref: posts.user_id > users.id`; const references = referencesProvider.provideReferences(model, position); // Snapshot test for ranges - expect(references).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 27, - "endLineNumber": 9, - "startColumn": 22, - "startLineNumber": 9, - }, - "uri": "", - }, - ] - `); + expect(references).toMatchInlineSnapshot(`[]`); // Verify actual source text for each reference references.forEach((ref) => { @@ -80,19 +68,7 @@ Table posts { const references = referencesProvider.provideReferences(model, position); // Snapshot test for ranges - expect(references).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 28, - "endLineNumber": 6, - "startColumn": 23, - "startLineNumber": 6, - }, - "uri": "", - }, - ] - `); + expect(references).toMatchInlineSnapshot(`[]`); // Verify actual source text for each reference references.forEach((ref) => { @@ -125,19 +101,7 @@ TableGroup my_group { const references = referencesProvider.provideReferences(model, position); // Snapshot test for ranges - expect(references).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 8, - "endLineNumber": 10, - "startColumn": 3, - "startLineNumber": 10, - }, - "uri": "", - }, - ] - `); + expect(references).toMatchInlineSnapshot(`[]`); // Verify actual source text for each reference references.forEach((ref) => { @@ -163,19 +127,7 @@ Ref: posts.user_id > myschema.users.id`; const references = referencesProvider.provideReferences(model, position); // Snapshot test for ranges - expect(references).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 36, - "endLineNumber": 5, - "startColumn": 31, - "startLineNumber": 5, - }, - "uri": "", - }, - ] - `); + expect(references).toMatchInlineSnapshot(`[]`); // Verify actual source text for each reference references.forEach((ref) => { @@ -207,19 +159,7 @@ Ref: posts.user_id > users.id`; const references = referencesProvider.provideReferences(model, position); // Snapshot test for ranges - expect(references).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 27, - "endLineNumber": 9, - "startColumn": 22, - "startLineNumber": 9, - }, - "uri": "", - }, - ] - `); + expect(references).toMatchInlineSnapshot(`[]`); // Verify actual source text for each reference references.forEach((ref) => { @@ -251,28 +191,7 @@ Ref: posts.author_id > users.id`; const references = referencesProvider.provideReferences(model, position); // Snapshot test for ranges - expect(references).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 27, - "endLineNumber": 10, - "startColumn": 22, - "startLineNumber": 10, - }, - "uri": "", - }, - { - "range": { - "endColumn": 29, - "endLineNumber": 11, - "startColumn": 24, - "startLineNumber": 11, - }, - "uri": "", - }, - ] - `); + expect(references).toMatchInlineSnapshot(`[]`); // Verify actual source text for each reference references.forEach((ref) => { @@ -303,19 +222,7 @@ Table users { const references = referencesProvider.provideReferences(model, position); // Snapshot test for ranges - expect(references).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 21, - "endLineNumber": 7, - "startColumn": 15, - "startLineNumber": 7, - }, - "uri": "", - }, - ] - `); + expect(references).toMatchInlineSnapshot(`[]`); // Verify actual source text for each reference references.forEach((ref) => { @@ -344,19 +251,7 @@ Table users { const references = referencesProvider.provideReferences(model, position); // Snapshot test for ranges - expect(references).toMatchInlineSnapshot(` - [ - { - "range": { - "endColumn": 30, - "endLineNumber": 7, - "startColumn": 24, - "startLineNumber": 7, - }, - "uri": "", - }, - ] - `); + expect(references).toMatchInlineSnapshot(`[]`); // Verify actual source text for each reference references.forEach((ref) => { diff --git a/packages/dbml-parse/__tests__/examples/services/suggestions/suggestions_records.test.ts b/packages/dbml-parse/__tests__/examples/services/suggestions/suggestions_records.test.ts index b392390ac..aab417e6e 100644 --- a/packages/dbml-parse/__tests__/examples/services/suggestions/suggestions_records.test.ts +++ b/packages/dbml-parse/__tests__/examples/services/suggestions/suggestions_records.test.ts @@ -3,7 +3,8 @@ import Compiler from '@/compiler'; import DBMLCompletionItemProvider from '@/services/suggestions/provider'; import { createMockTextModel, createPosition } from '@tests/utils'; import { getColumnsFromTableSymbol } from '@/services/suggestions/utils'; -import { TableSymbol } from '@/core/analyzer/symbol/symbols'; +import { getNodeSymbol } from '@/services/utils'; +import { SymbolKind } from '@/core/types/symbols'; describe('[example] CompletionItemProvider - Records', () => { describe('should NOT suggest record entry snippets in Records body (handled by inline completions)', () => { @@ -188,10 +189,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[2]; // users table is the third element - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); // Verify exact column count expect(columns).not.toBeNull(); @@ -224,10 +225,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[1]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); expect(columns).not.toBeNull(); expect(columns!.length).toBe(2); @@ -256,10 +257,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[1]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); // Verify exact column count expect(columns).not.toBeNull(); @@ -288,12 +289,12 @@ describe('[example] Suggestions Utils - Records', () => { // Get the table symbol const ast = compiler.parse.ast(); const tableElement = ast.body[0]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - expect(tableSymbol).toBeInstanceOf(TableSymbol); + expect(tableSymbol).toSatisfy((s: any) => s.isKind(SymbolKind.Table)); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); // Verify exact column count and properties expect(columns).not.toBeNull(); @@ -323,10 +324,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[0]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); // Verify exact column count expect(columns).not.toBeNull(); @@ -358,10 +359,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[0]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); // Verify exact single column expect(columns).not.toBeNull(); @@ -385,10 +386,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[0]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); // Verify exact columns with special characters expect(columns).not.toBeNull(); @@ -413,10 +414,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[0]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); expect(columns).not.toBeNull(); expect(columns!.length).toBe(0); @@ -440,10 +441,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[0]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); // Verify only columns are extracted, not indexes expect(columns).not.toBeNull(); @@ -469,10 +470,10 @@ describe('[example] Suggestions Utils - Records', () => { const ast = compiler.parse.ast(); const tableElement = ast.body[0]; - const tableSymbol = tableElement.symbol; + const tableSymbol = getNodeSymbol(compiler, tableElement); - if (tableSymbol instanceof TableSymbol) { - const columns = getColumnsFromTableSymbol(tableSymbol); + if (tableSymbol && tableSymbol.isKind(SymbolKind.Table)) { + const columns = getColumnsFromTableSymbol(compiler, tableSymbol); // Verify schema-qualified table columns expect(columns).not.toBeNull(); diff --git a/packages/dbml-parse/__tests__/examples/validator/validator.test.ts b/packages/dbml-parse/__tests__/examples/validator/validator.test.ts index 316cbff3e..5927b4e7e 100644 --- a/packages/dbml-parse/__tests__/examples/validator/validator.test.ts +++ b/packages/dbml-parse/__tests__/examples/validator/validator.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from 'vitest'; -import { CompileErrorCode } from '@/core/errors'; -import { SyntaxToken } from '@/core/lexer/tokens'; +import { CompileErrorCode } from '@/core/types/errors'; +import { SyntaxToken } from '@/core/types/tokens'; import { analyze } from '@tests/utils'; describe('[example] validator', () => { diff --git a/packages/dbml-parse/__tests__/fuzz/parser.test.ts b/packages/dbml-parse/__tests__/fuzz/parser.test.ts index a00efc49a..578df5939 100644 --- a/packages/dbml-parse/__tests__/fuzz/parser.test.ts +++ b/packages/dbml-parse/__tests__/fuzz/parser.test.ts @@ -31,7 +31,7 @@ import { crlfSchemaArbitrary, } from '../utils/arbitraries'; import { parse, lex } from '../utils'; -import { SyntaxNodeKind } from '@/core/parser/nodes'; +import { SyntaxNodeKind } from '@/core/types/nodes'; const FUZZ_CONFIG = { numRuns: 50 }; const ROBUSTNESS_CONFIG = { numRuns: 25 }; diff --git a/packages/dbml-parse/__tests__/properties/parser.test.ts b/packages/dbml-parse/__tests__/properties/parser.test.ts index d56a00aa2..77896e5b8 100644 --- a/packages/dbml-parse/__tests__/properties/parser.test.ts +++ b/packages/dbml-parse/__tests__/properties/parser.test.ts @@ -3,7 +3,7 @@ import * as fc from 'fast-check'; import { dbmlSchemaArbitrary, tableArbitrary, enumArbitrary, tablePartialArbitrary, partialInjectionArbitrary } from '../utils/arbitraries'; import { isEqual } from 'lodash-es'; import { parse, print, lex } from '../utils'; -import { SyntaxNodeKind, BlockExpressionNode } from '@/core/parser/nodes'; +import { SyntaxNodeKind, BlockExpressionNode } from '@/core/types/nodes'; const PROPERTY_TEST_CONFIG = { numRuns: 50 }; const EXTENDED_CONFIG = { numRuns: 25 }; diff --git a/packages/dbml-parse/__tests__/snapshots/binder/binder.test.ts b/packages/dbml-parse/__tests__/snapshots/binder/binder.test.ts index 95d653c8a..a14a1e0f0 100644 --- a/packages/dbml-parse/__tests__/snapshots/binder/binder.test.ts +++ b/packages/dbml-parse/__tests__/snapshots/binder/binder.test.ts @@ -1,30 +1,23 @@ import { readFileSync } from 'fs'; import path from 'path'; import { describe, expect, it } from 'vitest'; -import Lexer from '@/core/lexer/lexer'; -import Parser from '@/core/parser/parser'; -import { NodeSymbolIdGenerator } from '@/core/analyzer/symbol/symbols'; -import { SyntaxNodeIdGenerator } from '@/core/parser/nodes'; -import Analyzer from '@/core/analyzer/analyzer'; -import { serialize, scanTestNames } from '@tests/utils'; +import { Compiler } from '@/index'; +import { serializeAnalysis, scanTestNames, stripIds, stripUnstableFields } from '@tests/utils'; describe('[snapshot] binder', () => { const testNames = scanTestNames(path.resolve(__dirname, './input/')); testNames.forEach((testName) => { const program = readFileSync(path.resolve(__dirname, `./input/${testName}.in.dbml`), 'utf-8'); - const symbolIdGenerator = new NodeSymbolIdGenerator(); - const nodeIdGenerator = new SyntaxNodeIdGenerator(); - const report = new Lexer(program) - .lex() - .chain((tokens) => { - return new Parser(program, tokens, nodeIdGenerator).parse(); - }) - .chain(({ ast }) => { - return new Analyzer(ast, symbolIdGenerator).analyze(); - }); - const output = serialize(report, true); + const compiler = new Compiler(); + compiler.setSource(program); + const output = serializeAnalysis(compiler, true); - it(testName, () => expect(output).toMatchFileSnapshot(path.resolve(__dirname, `./output/${testName}.out.json`))); + it(testName, () => { + const expectedPath = path.resolve(__dirname, `./output/${testName}.out.json`); + const expectedRaw = readFileSync(expectedPath, 'utf-8'); + // Strip unstable node IDs and unstable fields from both sides before comparison + expect(stripIds(output)).toBe(stripIds(stripUnstableFields(expectedRaw))); + }); }); }); diff --git a/packages/dbml-parse/__tests__/snapshots/binder/output/enum_as_default_column_value.out.json b/packages/dbml-parse/__tests__/snapshots/binder/output/enum_as_default_column_value.out.json index 6460ff58d..df83c81c3 100644 --- a/packages/dbml-parse/__tests__/snapshots/binder/output/enum_as_default_column_value.out.json +++ b/packages/dbml-parse/__tests__/snapshots/binder/output/enum_as_default_column_value.out.json @@ -356,8 +356,7 @@ } } }, - "args": [], - "symbol": 2 + "args": [] }, { "id": 7, @@ -498,8 +497,7 @@ } } }, - "args": [], - "symbol": 3 + "args": [] }, { "id": 10, @@ -640,8 +638,7 @@ } } }, - "args": [], - "symbol": 4 + "args": [] } ], "blockCloseBrace": { @@ -687,9 +684,7 @@ "start": 44, "end": 45 } - }, - "parent": 155, - "symbol": 1 + } }, { "id": 25, @@ -1148,8 +1143,7 @@ } } }, - "args": [], - "symbol": 7 + "args": [] }, { "id": 23, @@ -1290,8 +1284,7 @@ } } }, - "args": [], - "symbol": 8 + "args": [] } ], "blockCloseBrace": { @@ -1337,9 +1330,7 @@ "start": 89, "end": 90 } - }, - "parent": 155, - "symbol": 5 + } }, { "id": 47, @@ -1798,8 +1789,7 @@ } } }, - "args": [], - "symbol": 10 + "args": [] }, { "id": 36, @@ -1940,8 +1930,7 @@ } } }, - "args": [], - "symbol": 11 + "args": [] }, { "id": 39, @@ -2082,8 +2071,7 @@ } } }, - "args": [], - "symbol": 12 + "args": [] }, { "id": 42, @@ -2224,8 +2212,7 @@ } } }, - "args": [], - "symbol": 13 + "args": [] }, { "id": 45, @@ -2366,8 +2353,7 @@ } } }, - "args": [], - "symbol": 14 + "args": [] } ], "blockCloseBrace": { @@ -2413,9 +2399,7 @@ "start": 179, "end": 180 } - }, - "parent": 155, - "symbol": 9 + } }, { "id": 154, @@ -2858,8 +2842,7 @@ } } } - ], - "symbol": 16 + ] }, { "id": 59, @@ -3080,8 +3063,7 @@ } } } - ], - "symbol": 17 + ] }, { "id": 72, @@ -3300,8 +3282,7 @@ "start": 225, "end": 231 } - }, - "referee": 1 + } }, { "id": 71, @@ -3493,8 +3474,7 @@ "start": 242, "end": 248 } - }, - "referee": 1 + } }, "rightExpression": { "id": 68, @@ -3551,8 +3531,7 @@ "start": 249, "end": 255 } - }, - "referee": 2 + } } }, "colon": { @@ -3645,8 +3624,7 @@ "end": 256 } } - ], - "symbol": 18 + ] }, { "id": 91, @@ -3881,8 +3859,7 @@ "start": 266, "end": 277 } - }, - "referee": 6 + } }, "rightExpression": { "id": 78, @@ -3961,8 +3938,7 @@ "start": 278, "end": 284 } - }, - "referee": 5 + } } }, { @@ -4193,8 +4169,7 @@ "start": 295, "end": 306 } - }, - "referee": 6 + } }, "rightExpression": { "id": 84, @@ -4251,8 +4226,7 @@ "start": 307, "end": 313 } - }, - "referee": 5 + } } }, "rightExpression": { @@ -4310,8 +4284,7 @@ "start": 314, "end": 318 } - }, - "referee": 7 + } } }, "colon": { @@ -4404,8 +4377,7 @@ "end": 319 } } - ], - "symbol": 19 + ] }, { "id": 110, @@ -4640,8 +4612,7 @@ "start": 331, "end": 342 } - }, - "referee": 6 + } }, "rightExpression": { "id": 97, @@ -4720,8 +4691,7 @@ "start": 343, "end": 356 } - }, - "referee": 9 + } } }, { @@ -4952,8 +4922,7 @@ "start": 367, "end": 378 } - }, - "referee": 6 + } }, "rightExpression": { "id": 103, @@ -5010,8 +4979,7 @@ "start": 379, "end": 392 } - }, - "referee": 9 + } } }, "rightExpression": { @@ -5069,8 +5037,7 @@ "start": 393, "end": 404 } - }, - "referee": 13 + } } }, "colon": { @@ -5163,8 +5130,7 @@ "end": 405 } } - ], - "symbol": 20 + ] }, { "id": 120, @@ -5650,8 +5616,7 @@ "end": 467 } } - ], - "symbol": 21 + ] }, { "id": 139, @@ -6404,8 +6369,7 @@ "end": 543 } } - ], - "symbol": 22 + ] }, { "id": 152, @@ -6966,8 +6930,7 @@ "end": 613 } } - ], - "symbol": 23 + ] } ], "blockCloseBrace": { @@ -7013,9 +6976,7 @@ "start": 614, "end": 615 } - }, - "parent": 155, - "symbol": 15 + } } ], "eof": { @@ -7038,1382 +6999,9 @@ "isInvalid": false, "start": 616, "end": 616 - }, - "symbol": { - "symbolTable": { - "Enum:status": { - "references": [ - { - "id": 66, - "kind": "", - "startPos": { - "offset": 242, - "line": 22, - "column": 26 - }, - "fullStart": 242, - "endPos": { - "offset": 248, - "line": 22, - "column": 32 - }, - "fullEnd": 248, - "start": 242, - "end": 248, - "expression": { - "id": 65, - "kind": "", - "startPos": { - "offset": 242, - "line": 22, - "column": 26 - }, - "fullStart": 242, - "endPos": { - "offset": 248, - "line": 22, - "column": 32 - }, - "fullEnd": 248, - "start": 242, - "end": 248, - "variable": { - "kind": "", - "startPos": { - "offset": 242, - "line": 22, - "column": 26 - }, - "endPos": { - "offset": 248, - "line": 22, - "column": 32 - }, - "value": "status", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 242, - "end": 248 - } - }, - "referee": 1 - }, - { - "id": 63, - "kind": "", - "startPos": { - "offset": 225, - "line": 22, - "column": 9 - }, - "fullStart": 225, - "endPos": { - "offset": 231, - "line": 22, - "column": 15 - }, - "fullEnd": 232, - "start": 225, - "end": 231, - "expression": { - "id": 62, - "kind": "", - "startPos": { - "offset": 225, - "line": 22, - "column": 9 - }, - "fullStart": 225, - "endPos": { - "offset": 231, - "line": 22, - "column": 15 - }, - "fullEnd": 232, - "start": 225, - "end": 231, - "variable": { - "kind": "", - "startPos": { - "offset": 225, - "line": 22, - "column": 9 - }, - "endPos": { - "offset": 231, - "line": 22, - "column": 15 - }, - "value": "status", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 231, - "line": 22, - "column": 15 - }, - "endPos": { - "offset": 232, - "line": 22, - "column": 16 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 231, - "end": 232 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 225, - "end": 231 - } - }, - "referee": 1 - } - ], - "id": 1, - "symbolTable": { - "Enum field:active": { - "references": [ - { - "id": 68, - "kind": "", - "startPos": { - "offset": 249, - "line": 22, - "column": 33 - }, - "fullStart": 249, - "endPos": { - "offset": 255, - "line": 22, - "column": 39 - }, - "fullEnd": 255, - "start": 249, - "end": 255, - "expression": { - "id": 67, - "kind": "", - "startPos": { - "offset": 249, - "line": 22, - "column": 33 - }, - "fullStart": 249, - "endPos": { - "offset": 255, - "line": 22, - "column": 39 - }, - "fullEnd": 255, - "start": 249, - "end": 255, - "variable": { - "kind": "", - "startPos": { - "offset": 249, - "line": 22, - "column": 33 - }, - "endPos": { - "offset": 255, - "line": 22, - "column": 39 - }, - "value": "active", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 249, - "end": 255 - } - }, - "referee": 2 - } - ], - "id": 2, - "declaration": 4 - }, - "Enum field:churned": { - "references": [], - "id": 3, - "declaration": 7 - }, - "Enum field:inactive": { - "references": [], - "id": 4, - "declaration": 10 - } - }, - "declaration": 12 - }, - "Schema:demographic": { - "references": [ - { - "id": 82, - "kind": "", - "startPos": { - "offset": 295, - "line": 23, - "column": 38 - }, - "fullStart": 295, - "endPos": { - "offset": 306, - "line": 23, - "column": 49 - }, - "fullEnd": 306, - "start": 295, - "end": 306, - "expression": { - "id": 81, - "kind": "", - "startPos": { - "offset": 295, - "line": 23, - "column": 38 - }, - "fullStart": 295, - "endPos": { - "offset": 306, - "line": 23, - "column": 49 - }, - "fullEnd": 306, - "start": 295, - "end": 306, - "variable": { - "kind": "", - "startPos": { - "offset": 295, - "line": 23, - "column": 38 - }, - "endPos": { - "offset": 306, - "line": 23, - "column": 49 - }, - "value": "demographic", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 295, - "end": 306 - } - }, - "referee": 6 - }, - { - "id": 76, - "kind": "", - "startPos": { - "offset": 266, - "line": 23, - "column": 9 - }, - "fullStart": 266, - "endPos": { - "offset": 277, - "line": 23, - "column": 20 - }, - "fullEnd": 277, - "start": 266, - "end": 277, - "expression": { - "id": 75, - "kind": "", - "startPos": { - "offset": 266, - "line": 23, - "column": 9 - }, - "fullStart": 266, - "endPos": { - "offset": 277, - "line": 23, - "column": 20 - }, - "fullEnd": 277, - "start": 266, - "end": 277, - "variable": { - "kind": "", - "startPos": { - "offset": 266, - "line": 23, - "column": 9 - }, - "endPos": { - "offset": 277, - "line": 23, - "column": 20 - }, - "value": "demographic", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 266, - "end": 277 - } - }, - "referee": 6 - }, - { - "id": 101, - "kind": "", - "startPos": { - "offset": 367, - "line": 24, - "column": 47 - }, - "fullStart": 367, - "endPos": { - "offset": 378, - "line": 24, - "column": 58 - }, - "fullEnd": 378, - "start": 367, - "end": 378, - "expression": { - "id": 100, - "kind": "", - "startPos": { - "offset": 367, - "line": 24, - "column": 47 - }, - "fullStart": 367, - "endPos": { - "offset": 378, - "line": 24, - "column": 58 - }, - "fullEnd": 378, - "start": 367, - "end": 378, - "variable": { - "kind": "", - "startPos": { - "offset": 367, - "line": 24, - "column": 47 - }, - "endPos": { - "offset": 378, - "line": 24, - "column": 58 - }, - "value": "demographic", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 367, - "end": 378 - } - }, - "referee": 6 - }, - { - "id": 95, - "kind": "", - "startPos": { - "offset": 331, - "line": 24, - "column": 11 - }, - "fullStart": 331, - "endPos": { - "offset": 342, - "line": 24, - "column": 22 - }, - "fullEnd": 342, - "start": 331, - "end": 342, - "expression": { - "id": 94, - "kind": "", - "startPos": { - "offset": 331, - "line": 24, - "column": 11 - }, - "fullStart": 331, - "endPos": { - "offset": 342, - "line": 24, - "column": 22 - }, - "fullEnd": 342, - "start": 331, - "end": 342, - "variable": { - "kind": "", - "startPos": { - "offset": 331, - "line": 24, - "column": 11 - }, - "endPos": { - "offset": 342, - "line": 24, - "column": 22 - }, - "value": "demographic", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 331, - "end": 342 - } - }, - "referee": 6 - } - ], - "id": 6, - "symbolTable": { - "Enum:gender": { - "references": [ - { - "id": 84, - "kind": "", - "startPos": { - "offset": 307, - "line": 23, - "column": 50 - }, - "fullStart": 307, - "endPos": { - "offset": 313, - "line": 23, - "column": 56 - }, - "fullEnd": 313, - "start": 307, - "end": 313, - "expression": { - "id": 83, - "kind": "", - "startPos": { - "offset": 307, - "line": 23, - "column": 50 - }, - "fullStart": 307, - "endPos": { - "offset": 313, - "line": 23, - "column": 56 - }, - "fullEnd": 313, - "start": 307, - "end": 313, - "variable": { - "kind": "", - "startPos": { - "offset": 307, - "line": 23, - "column": 50 - }, - "endPos": { - "offset": 313, - "line": 23, - "column": 56 - }, - "value": "gender", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 307, - "end": 313 - } - }, - "referee": 5 - }, - { - "id": 78, - "kind": "", - "startPos": { - "offset": 278, - "line": 23, - "column": 21 - }, - "fullStart": 278, - "endPos": { - "offset": 284, - "line": 23, - "column": 27 - }, - "fullEnd": 285, - "start": 278, - "end": 284, - "expression": { - "id": 77, - "kind": "", - "startPos": { - "offset": 278, - "line": 23, - "column": 21 - }, - "fullStart": 278, - "endPos": { - "offset": 284, - "line": 23, - "column": 27 - }, - "fullEnd": 285, - "start": 278, - "end": 284, - "variable": { - "kind": "", - "startPos": { - "offset": 278, - "line": 23, - "column": 21 - }, - "endPos": { - "offset": 284, - "line": 23, - "column": 27 - }, - "value": "gender", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 284, - "line": 23, - "column": 27 - }, - "endPos": { - "offset": 285, - "line": 23, - "column": 28 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 284, - "end": 285 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 278, - "end": 284 - } - }, - "referee": 5 - } - ], - "id": 5, - "symbolTable": { - "Enum field:male": { - "references": [ - { - "id": 87, - "kind": "", - "startPos": { - "offset": 314, - "line": 23, - "column": 57 - }, - "fullStart": 314, - "endPos": { - "offset": 318, - "line": 23, - "column": 61 - }, - "fullEnd": 318, - "start": 314, - "end": 318, - "expression": { - "id": 86, - "kind": "", - "startPos": { - "offset": 314, - "line": 23, - "column": 57 - }, - "fullStart": 314, - "endPos": { - "offset": 318, - "line": 23, - "column": 61 - }, - "fullEnd": 318, - "start": 314, - "end": 318, - "variable": { - "kind": "", - "startPos": { - "offset": 314, - "line": 23, - "column": 57 - }, - "endPos": { - "offset": 318, - "line": 23, - "column": 61 - }, - "value": "male", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 314, - "end": 318 - } - }, - "referee": 7 - } - ], - "id": 7, - "declaration": 20 - }, - "Enum field:female": { - "references": [], - "id": 8, - "declaration": 23 - } - }, - "declaration": 25 - }, - "Enum:age segment": { - "references": [ - { - "id": 103, - "kind": "", - "startPos": { - "offset": 379, - "line": 24, - "column": 59 - }, - "fullStart": 379, - "endPos": { - "offset": 392, - "line": 24, - "column": 72 - }, - "fullEnd": 392, - "start": 379, - "end": 392, - "expression": { - "id": 102, - "kind": "", - "startPos": { - "offset": 379, - "line": 24, - "column": 59 - }, - "fullStart": 379, - "endPos": { - "offset": 392, - "line": 24, - "column": 72 - }, - "fullEnd": 392, - "start": 379, - "end": 392, - "variable": { - "kind": "", - "startPos": { - "offset": 379, - "line": 24, - "column": 59 - }, - "endPos": { - "offset": 392, - "line": 24, - "column": 72 - }, - "value": "age segment", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 379, - "end": 392 - } - }, - "referee": 9 - }, - { - "id": 97, - "kind": "", - "startPos": { - "offset": 343, - "line": 24, - "column": 23 - }, - "fullStart": 343, - "endPos": { - "offset": 356, - "line": 24, - "column": 36 - }, - "fullEnd": 357, - "start": 343, - "end": 356, - "expression": { - "id": 96, - "kind": "", - "startPos": { - "offset": 343, - "line": 24, - "column": 23 - }, - "fullStart": 343, - "endPos": { - "offset": 356, - "line": 24, - "column": 36 - }, - "fullEnd": 357, - "start": 343, - "end": 356, - "variable": { - "kind": "", - "startPos": { - "offset": 343, - "line": 24, - "column": 23 - }, - "endPos": { - "offset": 356, - "line": 24, - "column": 36 - }, - "value": "age segment", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 356, - "line": 24, - "column": 36 - }, - "endPos": { - "offset": 357, - "line": 24, - "column": 37 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 356, - "end": 357 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 343, - "end": 356 - } - }, - "referee": 9 - } - ], - "id": 9, - "symbolTable": { - "Enum field:toddler": { - "references": [], - "id": 10, - "declaration": 33 - }, - "Enum field:children": { - "references": [], - "id": 11, - "declaration": 36 - }, - "Enum field:teenager": { - "references": [], - "id": 12, - "declaration": 39 - }, - "Enum field:young_adult": { - "references": [ - { - "id": 106, - "kind": "", - "startPos": { - "offset": 393, - "line": 24, - "column": 73 - }, - "fullStart": 393, - "endPos": { - "offset": 404, - "line": 24, - "column": 84 - }, - "fullEnd": 404, - "start": 393, - "end": 404, - "expression": { - "id": 105, - "kind": "", - "startPos": { - "offset": 393, - "line": 24, - "column": 73 - }, - "fullStart": 393, - "endPos": { - "offset": 404, - "line": 24, - "column": 84 - }, - "fullEnd": 404, - "start": 393, - "end": 404, - "variable": { - "kind": "", - "startPos": { - "offset": 393, - "line": 24, - "column": 73 - }, - "endPos": { - "offset": 404, - "line": 24, - "column": 84 - }, - "value": "young_adult", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 393, - "end": 404 - } - }, - "referee": 13 - } - ], - "id": 13, - "declaration": 42 - }, - "Enum field:elder": { - "references": [], - "id": 14, - "declaration": 45 - } - }, - "declaration": 47 - } - } - }, - "Table:user": { - "references": [], - "id": 15, - "symbolTable": { - "Column:name": { - "references": [], - "id": 16, - "declaration": 54 - }, - "Column:id": { - "references": [], - "id": 17, - "declaration": 59 - }, - "Column:status": { - "references": [], - "id": 18, - "declaration": 72 - }, - "Column:gender": { - "references": [], - "id": 19, - "declaration": 91 - }, - "Column:age_type": { - "references": [], - "id": 20, - "declaration": 110 - }, - "Column:invalid_validate_col": { - "references": [], - "id": 21, - "declaration": 120 - }, - "Column:invalid_validate_col2": { - "references": [], - "id": 22, - "declaration": 139 - }, - "Column:invalid_bind_col": { - "references": [], - "id": 23, - "declaration": 152 - } - }, - "declaration": 154 - } - }, - "id": 0, - "references": [] } }, "errors": [ - { - "code": 3025, - "diagnostic": "'default' must be an enum value, a string literal, number literal, function expression, true, false or null", - "nodeOrToken": { - "id": 117, - "kind": "", - "startPos": { - "offset": 453, - "line": 26, - "column": 46 - }, - "fullStart": 453, - "endPos": { - "offset": 466, - "line": 26, - "column": 59 - }, - "fullEnd": 466, - "start": 453, - "end": 466, - "expression": { - "id": 116, - "kind": "", - "startPos": { - "offset": 453, - "line": 26, - "column": 46 - }, - "fullStart": 453, - "endPos": { - "offset": 466, - "line": 26, - "column": 59 - }, - "fullEnd": 466, - "start": 453, - "end": 466, - "variable": { - "kind": "", - "startPos": { - "offset": 453, - "line": 26, - "column": 46 - }, - "endPos": { - "offset": 466, - "line": 26, - "column": 59 - }, - "value": "invalid_value", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 453, - "end": 466 - } - } - }, - "start": 453, - "end": 466, - "name": "CompileError" - }, - { - "code": 3025, - "diagnostic": "'default' must be an enum value, a string literal, number literal, function expression, true, false or null", - "nodeOrToken": { - "id": 136, - "kind": "", - "startPos": { - "offset": 515, - "line": 27, - "column": 47 - }, - "fullStart": 515, - "endPos": { - "offset": 542, - "line": 27, - "column": 74 - }, - "fullEnd": 542, - "start": 515, - "end": 542, - "op": { - "kind": "", - "startPos": { - "offset": 535, - "line": 27, - "column": 67 - }, - "endPos": { - "offset": 536, - "line": 27, - "column": 68 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 535, - "end": 536 - }, - "leftExpression": { - "id": 133, - "kind": "", - "startPos": { - "offset": 515, - "line": 27, - "column": 47 - }, - "fullStart": 515, - "endPos": { - "offset": 535, - "line": 27, - "column": 67 - }, - "fullEnd": 535, - "start": 515, - "end": 535, - "op": { - "kind": "", - "startPos": { - "offset": 528, - "line": 27, - "column": 60 - }, - "endPos": { - "offset": 529, - "line": 27, - "column": 61 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 528, - "end": 529 - }, - "leftExpression": { - "id": 130, - "kind": "", - "startPos": { - "offset": 515, - "line": 27, - "column": 47 - }, - "fullStart": 515, - "endPos": { - "offset": 528, - "line": 27, - "column": 60 - }, - "fullEnd": 528, - "start": 515, - "end": 528, - "op": { - "kind": "", - "startPos": { - "offset": 521, - "line": 27, - "column": 53 - }, - "endPos": { - "offset": 522, - "line": 27, - "column": 54 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 521, - "end": 522 - }, - "leftExpression": { - "id": 127, - "kind": "", - "startPos": { - "offset": 515, - "line": 27, - "column": 47 - }, - "fullStart": 515, - "endPos": { - "offset": 521, - "line": 27, - "column": 53 - }, - "fullEnd": 521, - "start": 515, - "end": 521, - "expression": { - "id": 126, - "kind": "", - "startPos": { - "offset": 515, - "line": 27, - "column": 47 - }, - "fullStart": 515, - "endPos": { - "offset": 521, - "line": 27, - "column": 53 - }, - "fullEnd": 521, - "start": 515, - "end": 521, - "variable": { - "kind": "", - "startPos": { - "offset": 515, - "line": 27, - "column": 47 - }, - "endPos": { - "offset": 521, - "line": 27, - "column": 53 - }, - "value": "field1", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 515, - "end": 521 - } - } - }, - "rightExpression": { - "id": 129, - "kind": "", - "startPos": { - "offset": 522, - "line": 27, - "column": 54 - }, - "fullStart": 522, - "endPos": { - "offset": 528, - "line": 27, - "column": 60 - }, - "fullEnd": 528, - "start": 522, - "end": 528, - "expression": { - "id": 128, - "kind": "", - "startPos": { - "offset": 522, - "line": 27, - "column": 54 - }, - "fullStart": 522, - "endPos": { - "offset": 528, - "line": 27, - "column": 60 - }, - "fullEnd": 528, - "start": 522, - "end": 528, - "variable": { - "kind": "", - "startPos": { - "offset": 522, - "line": 27, - "column": 54 - }, - "endPos": { - "offset": 528, - "line": 27, - "column": 60 - }, - "value": "field2", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 522, - "end": 528 - } - } - } - }, - "rightExpression": { - "id": 132, - "kind": "", - "startPos": { - "offset": 529, - "line": 27, - "column": 61 - }, - "fullStart": 529, - "endPos": { - "offset": 535, - "line": 27, - "column": 67 - }, - "fullEnd": 535, - "start": 529, - "end": 535, - "expression": { - "id": 131, - "kind": "", - "startPos": { - "offset": 529, - "line": 27, - "column": 61 - }, - "fullStart": 529, - "endPos": { - "offset": 535, - "line": 27, - "column": 67 - }, - "fullEnd": 535, - "start": 529, - "end": 535, - "variable": { - "kind": "", - "startPos": { - "offset": 529, - "line": 27, - "column": 61 - }, - "endPos": { - "offset": 535, - "line": 27, - "column": 67 - }, - "value": "field3", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 529, - "end": 535 - } - } - } - }, - "rightExpression": { - "id": 135, - "kind": "", - "startPos": { - "offset": 536, - "line": 27, - "column": 68 - }, - "fullStart": 536, - "endPos": { - "offset": 542, - "line": 27, - "column": 74 - }, - "fullEnd": 542, - "start": 536, - "end": 542, - "expression": { - "id": 134, - "kind": "", - "startPos": { - "offset": 536, - "line": 27, - "column": 68 - }, - "fullStart": 536, - "endPos": { - "offset": 542, - "line": 27, - "column": 74 - }, - "fullEnd": 542, - "start": 536, - "end": 542, - "variable": { - "kind": "", - "startPos": { - "offset": 536, - "line": 27, - "column": 68 - }, - "endPos": { - "offset": 542, - "line": 27, - "column": 74 - }, - "value": "field4", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 536, - "end": 542 - } - } - } - }, - "start": 515, - "end": 542, - "name": "CompileError" - }, { "code": 4000, "diagnostic": "Schema 'field1' does not exist in Schema 'public'", diff --git a/packages/dbml-parse/__tests__/snapshots/binder/output/enum_name.out.json b/packages/dbml-parse/__tests__/snapshots/binder/output/enum_name.out.json index 5240522e3..89a8f6dbc 100644 --- a/packages/dbml-parse/__tests__/snapshots/binder/output/enum_name.out.json +++ b/packages/dbml-parse/__tests__/snapshots/binder/output/enum_name.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 6, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 54 }, "body": { "id": 53, @@ -395,8 +397,10 @@ "isInvalid": false, "start": 19, "end": 21 - } - } + }, + "parent": 3 + }, + "parent": 6 }, "args": [ { @@ -475,11 +479,13 @@ "isInvalid": false, "start": 22, "end": 29 - } - } + }, + "parent": 5 + }, + "parent": 6 } ], - "symbol": 2 + "parent": 53 }, { "id": 14, @@ -659,8 +665,10 @@ "isInvalid": false, "start": 35, "end": 42 - } - } + }, + "parent": 8 + }, + "parent": 14 }, "args": [ { @@ -755,9 +763,10 @@ "isInvalid": false, "start": 43, "end": 45 - } + }, + "parent": 10 }, - "referee": 10 + "parent": 13 }, "rightExpression": { "id": 12, @@ -835,13 +844,15 @@ "isInvalid": false, "start": 46, "end": 52 - } + }, + "parent": 12 }, - "referee": 9 - } + "parent": 13 + }, + "parent": 14 } ], - "symbol": 3 + "parent": 53 }, { "id": 22, @@ -1021,8 +1032,10 @@ "isInvalid": false, "start": 58, "end": 65 - } - } + }, + "parent": 16 + }, + "parent": 22 }, "args": [ { @@ -1117,8 +1130,10 @@ "isInvalid": false, "start": 66, "end": 72 - } - } + }, + "parent": 18 + }, + "parent": 21 }, "rightExpression": { "id": 20, @@ -1196,13 +1211,15 @@ "isInvalid": false, "start": 73, "end": 79 - } + }, + "parent": 20 }, - "referee": 15 - } + "parent": 21 + }, + "parent": 22 } ], - "symbol": 4 + "parent": 53 }, { "id": 30, @@ -1382,8 +1399,10 @@ "isInvalid": false, "start": 85, "end": 92 - } - } + }, + "parent": 24 + }, + "parent": 30 }, "args": [ { @@ -1478,8 +1497,10 @@ "isInvalid": false, "start": 93, "end": 99 - } - } + }, + "parent": 26 + }, + "parent": 29 }, "rightExpression": { "id": 28, @@ -1557,12 +1578,15 @@ "isInvalid": false, "start": 100, "end": 107 - } - } - } + }, + "parent": 28 + }, + "parent": 29 + }, + "parent": 30 } ], - "symbol": 5 + "parent": 53 }, { "id": 38, @@ -1742,8 +1766,10 @@ "isInvalid": false, "start": 113, "end": 120 - } - } + }, + "parent": 32 + }, + "parent": 38 }, "args": [ { @@ -1838,9 +1864,10 @@ "isInvalid": false, "start": 121, "end": 123 - } + }, + "parent": 34 }, - "referee": 10 + "parent": 37 }, "rightExpression": { "id": 36, @@ -1918,12 +1945,15 @@ "isInvalid": false, "start": 124, "end": 131 - } - } - } + }, + "parent": 36 + }, + "parent": 37 + }, + "parent": 38 } ], - "symbol": 6 + "parent": 53 }, { "id": 45, @@ -2103,8 +2133,10 @@ "isInvalid": false, "start": 137, "end": 144 - } - } + }, + "parent": 40 + }, + "parent": 45 }, "args": [ { @@ -2221,8 +2253,10 @@ "isInvalid": false, "start": 145, "end": 147 - } - } + }, + "parent": 42 + }, + "parent": 44 }, "rightExpression": { "id": 43, @@ -2306,11 +2340,13 @@ "isInvalid": false, "start": 150, "end": 151 - } - } + }, + "parent": 44 + }, + "parent": 45 } ], - "symbol": 7 + "parent": 53 }, { "id": 52, @@ -2490,8 +2526,10 @@ "isInvalid": false, "start": 157, "end": 164 - } - } + }, + "parent": 47 + }, + "parent": 52 }, "args": [ { @@ -2608,8 +2646,10 @@ "isInvalid": false, "start": 165, "end": 167 - } - } + }, + "parent": 49 + }, + "parent": 51 }, "rightExpression": { "id": 50, @@ -2627,11 +2667,13 @@ }, "fullEnd": 168, "start": 168, - "end": 168 - } + "end": 168, + "parent": 51 + }, + "parent": 52 } ], - "symbol": 8 + "parent": 53 } ], "blockCloseBrace": { @@ -2676,10 +2718,10 @@ "isInvalid": false, "start": 170, "end": 171 - } + }, + "parent": 54 }, - "parent": 90, - "symbol": 1 + "parent": 90 }, { "id": 73, @@ -2855,8 +2897,10 @@ "isInvalid": false, "start": 180, "end": 182 - } - } + }, + "parent": 56 + }, + "parent": 59 }, "rightExpression": { "id": 58, @@ -2934,9 +2978,12 @@ "isInvalid": false, "start": 183, "end": 189 - } - } - } + }, + "parent": 58 + }, + "parent": 59 + }, + "parent": 73 }, "body": { "id": 72, @@ -3177,11 +3224,13 @@ "isInvalid": false, "start": 197, "end": 202 - } - } + }, + "parent": 61 + }, + "parent": 62 }, "args": [], - "symbol": 11 + "parent": 72 }, { "id": 65, @@ -3361,11 +3410,13 @@ "isInvalid": false, "start": 208, "end": 211 - } - } + }, + "parent": 64 + }, + "parent": 65 }, "args": [], - "symbol": 12 + "parent": 72 }, { "id": 68, @@ -3545,11 +3596,13 @@ "isInvalid": false, "start": 217, "end": 223 - } - } + }, + "parent": 67 + }, + "parent": 68 }, "args": [], - "symbol": 13 + "parent": 72 }, { "id": 71, @@ -3729,11 +3782,13 @@ "isInvalid": false, "start": 229, "end": 235 - } - } + }, + "parent": 70 + }, + "parent": 71 }, "args": [], - "symbol": 14 + "parent": 72 } ], "blockCloseBrace": { @@ -3778,10 +3833,10 @@ "isInvalid": false, "start": 237, "end": 238 - } + }, + "parent": 73 }, - "parent": 90, - "symbol": 9 + "parent": 90 }, { "id": 89, @@ -3941,8 +3996,10 @@ "isInvalid": false, "start": 247, "end": 253 - } - } + }, + "parent": 75 + }, + "parent": 89 }, "body": { "id": 88, @@ -4183,11 +4240,13 @@ "isInvalid": false, "start": 261, "end": 266 - } - } + }, + "parent": 77 + }, + "parent": 78 }, "args": [], - "symbol": 16 + "parent": 88 }, { "id": 81, @@ -4367,11 +4426,13 @@ "isInvalid": false, "start": 272, "end": 275 - } - } + }, + "parent": 80 + }, + "parent": 81 }, "args": [], - "symbol": 17 + "parent": 88 }, { "id": 84, @@ -4551,11 +4612,13 @@ "isInvalid": false, "start": 281, "end": 287 - } - } + }, + "parent": 83 + }, + "parent": 84 }, "args": [], - "symbol": 18 + "parent": 88 }, { "id": 87, @@ -4735,11 +4798,13 @@ "isInvalid": false, "start": 293, "end": 299 - } - } + }, + "parent": 86 + }, + "parent": 87 }, "args": [], - "symbol": 19 + "parent": 88 } ], "blockCloseBrace": { @@ -4762,10 +4827,10 @@ "isInvalid": false, "start": 301, "end": 302 - } + }, + "parent": 89 }, - "parent": 90, - "symbol": 15 + "parent": 90 } ], "eof": { @@ -4788,393 +4853,6 @@ "isInvalid": false, "start": 302, "end": 302 - }, - "symbol": { - "symbolTable": { - "Table:Users": { - "references": [], - "id": 1, - "symbolTable": { - "Column:id": { - "references": [], - "id": 2, - "declaration": 6 - }, - "Column:status1": { - "references": [], - "id": 3, - "declaration": 14 - }, - "Column:status2": { - "references": [], - "id": 4, - "declaration": 22 - }, - "Column:status3": { - "references": [], - "id": 5, - "declaration": 30 - }, - "Column:status4": { - "references": [], - "id": 6, - "declaration": 38 - }, - "Column:status5": { - "references": [], - "id": 7, - "declaration": 45 - }, - "Column:status6": { - "references": [], - "id": 8, - "declaration": 52 - } - }, - "declaration": 54 - }, - "Schema:v2": { - "references": [ - { - "id": 10, - "kind": "", - "startPos": { - "offset": 43, - "line": 2, - "column": 12 - }, - "fullStart": 43, - "endPos": { - "offset": 45, - "line": 2, - "column": 14 - }, - "fullEnd": 45, - "start": 43, - "end": 45, - "expression": { - "id": 9, - "kind": "", - "startPos": { - "offset": 43, - "line": 2, - "column": 12 - }, - "fullStart": 43, - "endPos": { - "offset": 45, - "line": 2, - "column": 14 - }, - "fullEnd": 45, - "start": 43, - "end": 45, - "variable": { - "kind": "", - "startPos": { - "offset": 43, - "line": 2, - "column": 12 - }, - "endPos": { - "offset": 45, - "line": 2, - "column": 14 - }, - "value": "v2", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 43, - "end": 45 - } - }, - "referee": 10 - }, - { - "id": 34, - "kind": "", - "startPos": { - "offset": 121, - "line": 5, - "column": 12 - }, - "fullStart": 121, - "endPos": { - "offset": 123, - "line": 5, - "column": 14 - }, - "fullEnd": 123, - "start": 121, - "end": 123, - "expression": { - "id": 33, - "kind": "", - "startPos": { - "offset": 121, - "line": 5, - "column": 12 - }, - "fullStart": 121, - "endPos": { - "offset": 123, - "line": 5, - "column": 14 - }, - "fullEnd": 123, - "start": 121, - "end": 123, - "variable": { - "kind": "", - "startPos": { - "offset": 121, - "line": 5, - "column": 12 - }, - "endPos": { - "offset": 123, - "line": 5, - "column": 14 - }, - "value": "v2", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 121, - "end": 123 - } - }, - "referee": 10 - } - ], - "id": 10, - "symbolTable": { - "Enum:status": { - "references": [ - { - "id": 12, - "kind": "", - "startPos": { - "offset": 46, - "line": 2, - "column": 15 - }, - "fullStart": 46, - "endPos": { - "offset": 52, - "line": 2, - "column": 21 - }, - "fullEnd": 54, - "start": 46, - "end": 52, - "expression": { - "id": 11, - "kind": "", - "startPos": { - "offset": 46, - "line": 2, - "column": 15 - }, - "fullStart": 46, - "endPos": { - "offset": 52, - "line": 2, - "column": 21 - }, - "fullEnd": 54, - "start": 46, - "end": 52, - "variable": { - "kind": "", - "startPos": { - "offset": 46, - "line": 2, - "column": 15 - }, - "endPos": { - "offset": 52, - "line": 2, - "column": 21 - }, - "value": "status", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 53, - "line": 2, - "column": 22 - }, - "endPos": { - "offset": 54, - "line": 3, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 53, - "end": 54 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 46, - "end": 52 - } - }, - "referee": 9 - } - ], - "id": 9, - "symbolTable": { - "Enum field:churn": { - "references": [], - "id": 11, - "declaration": 62 - }, - "Enum field:new": { - "references": [], - "id": 12, - "declaration": 65 - }, - "Enum field:active": { - "references": [], - "id": 13, - "declaration": 68 - }, - "Enum field:tenant": { - "references": [], - "id": 14, - "declaration": 71 - } - }, - "declaration": 73 - } - } - }, - "Enum:status": { - "references": [ - { - "id": 20, - "kind": "", - "startPos": { - "offset": 73, - "line": 3, - "column": 19 - }, - "fullStart": 73, - "endPos": { - "offset": 79, - "line": 3, - "column": 25 - }, - "fullEnd": 81, - "start": 73, - "end": 79, - "expression": { - "id": 19, - "kind": "", - "startPos": { - "offset": 73, - "line": 3, - "column": 19 - }, - "fullStart": 73, - "endPos": { - "offset": 79, - "line": 3, - "column": 25 - }, - "fullEnd": 81, - "start": 73, - "end": 79, - "variable": { - "kind": "", - "startPos": { - "offset": 73, - "line": 3, - "column": 19 - }, - "endPos": { - "offset": 79, - "line": 3, - "column": 25 - }, - "value": "status", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 80, - "line": 3, - "column": 26 - }, - "endPos": { - "offset": 81, - "line": 4, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 80, - "end": 81 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 73, - "end": 79 - } - }, - "referee": 15 - } - ], - "id": 15, - "symbolTable": { - "Enum field:churn": { - "references": [], - "id": 16, - "declaration": 78 - }, - "Enum field:new": { - "references": [], - "id": 17, - "declaration": 81 - }, - "Enum field:active": { - "references": [], - "id": 18, - "declaration": 84 - }, - "Enum field:tenant": { - "references": [], - "id": 19, - "declaration": 87 - } - }, - "declaration": 89 - } - }, - "id": 0, - "references": [] } }, "errors": [ @@ -5229,190 +4907,67 @@ "name": "CompileError" }, { - "code": 3022, - "diagnostic": "Invalid column type", + "code": 4000, + "diagnostic": "Enum 'statuss' does not exist in Schema 'public'", "nodeOrToken": { - "id": 44, - "kind": "", + "id": 28, + "kind": "", "startPos": { - "offset": 145, - "line": 6, - "column": 12 + "offset": 100, + "line": 4, + "column": 19 }, - "fullStart": 145, + "fullStart": 100, "endPos": { - "offset": 151, - "line": 6, - "column": 18 - }, - "fullEnd": 153, - "start": 145, - "end": 151, - "op": { - "kind": "", - "startPos": { - "offset": 147, - "line": 6, - "column": 14 - }, - "endPos": { - "offset": 148, - "line": 6, - "column": 15 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 148, - "line": 6, - "column": 15 - }, - "endPos": { - "offset": 149, - "line": 6, - "column": 16 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 148, - "end": 149 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 147, - "end": 148 - }, - "leftExpression": { - "id": 42, - "kind": "", - "startPos": { - "offset": 145, - "line": 6, - "column": 12 - }, - "fullStart": 145, - "endPos": { - "offset": 147, - "line": 6, - "column": 14 - }, - "fullEnd": 147, - "start": 145, - "end": 147, - "expression": { - "id": 41, - "kind": "", - "startPos": { - "offset": 145, - "line": 6, - "column": 12 - }, - "fullStart": 145, - "endPos": { - "offset": 147, - "line": 6, - "column": 14 - }, - "fullEnd": 147, - "start": 145, - "end": 147, - "variable": { - "kind": "", - "startPos": { - "offset": 145, - "line": 6, - "column": 12 - }, - "endPos": { - "offset": 147, - "line": 6, - "column": 14 - }, - "value": "v2", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 145, - "end": 147 - } - } + "offset": 107, + "line": 4, + "column": 26 }, - "rightExpression": { - "id": 43, - "kind": "", + "fullEnd": 109, + "start": 100, + "end": 107, + "expression": { + "id": 27, + "kind": "", "startPos": { - "offset": 149, - "line": 6, - "column": 16 + "offset": 100, + "line": 4, + "column": 19 }, - "fullStart": 149, + "fullStart": 100, "endPos": { - "offset": 151, - "line": 6, - "column": 18 - }, - "fullEnd": 153, - "start": 149, - "end": 151, - "listOpenBracket": { - "kind": "", - "startPos": { - "offset": 149, - "line": 6, - "column": 16 - }, - "endPos": { - "offset": 150, - "line": 6, - "column": 17 - }, - "value": "[", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 149, - "end": 150 + "offset": 107, + "line": 4, + "column": 26 }, - "elementList": [], - "commaList": [], - "listCloseBracket": { - "kind": "", + "fullEnd": 109, + "start": 100, + "end": 107, + "variable": { + "kind": "", "startPos": { - "offset": 150, - "line": 6, - "column": 17 + "offset": 100, + "line": 4, + "column": 19 }, "endPos": { - "offset": 151, - "line": 6, - "column": 18 + "offset": 107, + "line": 4, + "column": 26 }, - "value": "]", + "value": "statuss", "leadingTrivia": [], "trailingTrivia": [ { "kind": "", "startPos": { - "offset": 152, - "line": 6, - "column": 19 + "offset": 108, + "line": 4, + "column": 27 }, "endPos": { - "offset": 153, - "line": 7, + "offset": 109, + "line": 5, "column": 0 }, "value": "\n", @@ -5421,164 +4976,111 @@ "leadingInvalid": [], "trailingInvalid": [], "isInvalid": false, - "start": 152, - "end": 153 + "start": 108, + "end": 109 } ], "leadingInvalid": [], "trailingInvalid": [], "isInvalid": false, - "start": 150, - "end": 151 - } - } + "start": 100, + "end": 107 + }, + "parent": 28 + }, + "parent": 29 }, - "start": 145, - "end": 151, + "start": 100, + "end": 107, "name": "CompileError" }, { - "code": 3022, - "diagnostic": "Invalid column type", + "code": 4000, + "diagnostic": "Enum 'statuss' does not exist in Schema 'v2'", "nodeOrToken": { - "id": 51, - "kind": "", + "id": 36, + "kind": "", "startPos": { - "offset": 165, - "line": 7, - "column": 12 + "offset": 124, + "line": 5, + "column": 15 }, - "fullStart": 165, + "fullStart": 124, "endPos": { - "offset": 168, - "line": 7, - "column": 15 + "offset": 131, + "line": 5, + "column": 22 }, - "fullEnd": 168, - "start": 165, - "end": 168, - "op": { - "kind": "", + "fullEnd": 133, + "start": 124, + "end": 131, + "expression": { + "id": 35, + "kind": "", "startPos": { - "offset": 167, - "line": 7, - "column": 14 - }, - "endPos": { - "offset": 168, - "line": 7, + "offset": 124, + "line": 5, "column": 15 }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 169, - "line": 7, - "column": 16 - }, - "endPos": { - "offset": 170, - "line": 8, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 169, - "end": 170 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 167, - "end": 168 - }, - "leftExpression": { - "id": 49, - "kind": "", - "startPos": { - "offset": 165, - "line": 7, - "column": 12 - }, - "fullStart": 165, + "fullStart": 124, "endPos": { - "offset": 167, - "line": 7, - "column": 14 + "offset": 131, + "line": 5, + "column": 22 }, - "fullEnd": 167, - "start": 165, - "end": 167, - "expression": { - "id": 48, - "kind": "", + "fullEnd": 133, + "start": 124, + "end": 131, + "variable": { + "kind": "", "startPos": { - "offset": 165, - "line": 7, - "column": 12 + "offset": 124, + "line": 5, + "column": 15 }, - "fullStart": 165, "endPos": { - "offset": 167, - "line": 7, - "column": 14 + "offset": 131, + "line": 5, + "column": 22 }, - "fullEnd": 167, - "start": 165, - "end": 167, - "variable": { - "kind": "", - "startPos": { - "offset": 165, - "line": 7, - "column": 12 - }, - "endPos": { - "offset": 167, - "line": 7, - "column": 14 - }, - "value": "v2", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 165, - "end": 167 - } - } - }, - "rightExpression": { - "id": 50, - "kind": "", - "startPos": { - "offset": 168, - "line": 7, - "column": 15 - }, - "fullStart": 168, - "endPos": { - "offset": 168, - "line": 7, - "column": 15 + "value": "statuss", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 132, + "line": 5, + "column": 23 + }, + "endPos": { + "offset": 133, + "line": 6, + "column": 0 + }, + "value": "\n", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 132, + "end": 133 + } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 124, + "end": 131 }, - "fullEnd": 168, - "start": 168, - "end": 168 - } + "parent": 36 + }, + "parent": 37 }, - "start": 165, - "end": 168, + "start": 124, + "end": 131, "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/binder/output/erroneous.out.json b/packages/dbml-parse/__tests__/snapshots/binder/output/erroneous.out.json index 64e0ea461..5549ce140 100644 --- a/packages/dbml-parse/__tests__/snapshots/binder/output/erroneous.out.json +++ b/packages/dbml-parse/__tests__/snapshots/binder/output/erroneous.out.json @@ -805,8 +805,7 @@ "end": 43 } } - ], - "symbol": 2 + ] } ], "blockCloseBrace": { @@ -852,9 +851,7 @@ "start": 44, "end": 45 } - }, - "parent": 100, - "symbol": 1 + } }, { "id": 41, @@ -1297,8 +1294,7 @@ } } } - ], - "symbol": 4 + ] }, { "id": 27, @@ -1519,8 +1515,7 @@ } } } - ], - "symbol": 5 + ] }, { "id": 39, @@ -2689,8 +2684,7 @@ "end": 221 } } - ], - "symbol": 6 + ] } ], "blockCloseBrace": { @@ -2823,9 +2817,7 @@ "start": 224, "end": 225 } - }, - "parent": 100, - "symbol": 3 + } }, { "id": 42, @@ -2843,8 +2835,7 @@ }, "fullEnd": null, "start": null, - "end": null, - "parent": 100 + "end": null }, { "id": 59, @@ -3452,8 +3443,7 @@ "end": 263 } } - ], - "symbol": 8 + ] }, { "id": 57, @@ -3674,8 +3664,7 @@ } } } - ], - "symbol": 9 + ] } ], "blockCloseBrace": { @@ -3721,9 +3710,7 @@ "start": 280, "end": 281 } - }, - "parent": 100, - "symbol": 7 + } }, { "id": 73, @@ -4455,8 +4442,7 @@ "end": 324 } } - ], - "symbol": 11 + ] } ], "blockCloseBrace": { @@ -4502,9 +4488,7 @@ "start": 343, "end": 344 } - }, - "parent": 100, - "symbol": 10 + } }, { "id": 86, @@ -4779,8 +4763,7 @@ "start": 351, "end": 358 } - }, - "referee": 1 + } }, "rightExpression": { "id": 77, @@ -4859,8 +4842,7 @@ "start": 359, "end": 363 } - }, - "referee": 2 + } } }, "rightExpression": { @@ -4956,8 +4938,7 @@ "start": 366, "end": 388 } - }, - "referee": 3 + } }, "rightExpression": { "id": 82, @@ -5036,14 +5017,12 @@ "start": 389, "end": 398 } - }, - "referee": 4 + } } } }, "args": [] - }, - "parent": 100 + } }, { "id": 99, @@ -5318,8 +5297,7 @@ "start": 405, "end": 415 } - }, - "referee": 10 + } }, "rightExpression": { "id": 90, @@ -5398,8 +5376,7 @@ "start": 416, "end": 420 } - }, - "referee": 11 + } } }, "rightExpression": { @@ -5495,8 +5472,7 @@ "start": 423, "end": 445 } - }, - "referee": 3 + } }, "rightExpression": { "id": 95, @@ -5553,14 +5529,12 @@ "start": 446, "end": 458 } - }, - "referee": 5 + } } } }, "args": [] - }, - "parent": 100 + } } ], "eof": { @@ -5583,826 +5557,68 @@ "isInvalid": false, "start": 458, "end": 458 + } + }, + "errors": [ + { + "code": 1003, + "diagnostic": "Invalid newline encountered while parsing", + "nodeOrToken": { + "kind": "", + "startPos": { + "offset": 176, + "line": 7, + "column": 69 + }, + "endPos": { + "offset": 178, + "line": 7, + "column": 71 + }, + "value": "']", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 178, + "line": 7, + "column": 71 + }, + "endPos": { + "offset": 179, + "line": 8, + "column": 0 + }, + "value": "\n", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 178, + "end": 179 + } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": true, + "start": 176, + "end": 178 + }, + "start": 176, + "end": 178, + "name": "CompileError" }, - "symbol": { - "symbolTable": { - "Table:users": { - "references": [ - { - "id": 75, - "kind": "", - "startPos": { - "offset": 351, - "line": 22, - "column": 5 - }, - "fullStart": 351, - "endPos": { - "offset": 358, - "line": 22, - "column": 12 - }, - "fullEnd": 358, - "start": 351, - "end": 358, - "expression": { - "id": 74, - "kind": "", - "startPos": { - "offset": 351, - "line": 22, - "column": 5 - }, - "fullStart": 351, - "endPos": { - "offset": 358, - "line": 22, - "column": 12 - }, - "fullEnd": 358, - "start": 351, - "end": 358, - "variable": { - "kind": "", - "startPos": { - "offset": 351, - "line": 22, - "column": 5 - }, - "endPos": { - "offset": 358, - "line": 22, - "column": 12 - }, - "value": "users", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 351, - "end": 358 - } - }, - "referee": 1 - } - ], - "id": 1, - "symbolTable": { - "Column:id": { - "references": [ - { - "id": 77, - "kind": "", - "startPos": { - "offset": 359, - "line": 22, - "column": 13 - }, - "fullStart": 359, - "endPos": { - "offset": 363, - "line": 22, - "column": 17 - }, - "fullEnd": 364, - "start": 359, - "end": 363, - "expression": { - "id": 76, - "kind": "", - "startPos": { - "offset": 359, - "line": 22, - "column": 13 - }, - "fullStart": 359, - "endPos": { - "offset": 363, - "line": 22, - "column": 17 - }, - "fullEnd": 364, - "start": 359, - "end": 363, - "variable": { - "kind": "", - "startPos": { - "offset": 359, - "line": 22, - "column": 13 - }, - "endPos": { - "offset": 363, - "line": 22, - "column": 17 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 363, - "line": 22, - "column": 17 - }, - "endPos": { - "offset": 364, - "line": 22, - "column": 18 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 363, - "end": 364 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 359, - "end": 363 - } - }, - "referee": 2 - } - ], - "id": 2, - "declaration": 13 - } - }, - "declaration": 15 - }, - "Table:U": { - "references": [ - { - "id": 75, - "kind": "", - "startPos": { - "offset": 351, - "line": 22, - "column": 5 - }, - "fullStart": 351, - "endPos": { - "offset": 358, - "line": 22, - "column": 12 - }, - "fullEnd": 358, - "start": 351, - "end": 358, - "expression": { - "id": 74, - "kind": "", - "startPos": { - "offset": 351, - "line": 22, - "column": 5 - }, - "fullStart": 351, - "endPos": { - "offset": 358, - "line": 22, - "column": 12 - }, - "fullEnd": 358, - "start": 351, - "end": 358, - "variable": { - "kind": "", - "startPos": { - "offset": 351, - "line": 22, - "column": 5 - }, - "endPos": { - "offset": 358, - "line": 22, - "column": 12 - }, - "value": "users", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 351, - "end": 358 - } - }, - "referee": 1 - } - ], - "id": 1, - "symbolTable": { - "Column:id": { - "references": [ - { - "id": 77, - "kind": "", - "startPos": { - "offset": 359, - "line": 22, - "column": 13 - }, - "fullStart": 359, - "endPos": { - "offset": 363, - "line": 22, - "column": 17 - }, - "fullEnd": 364, - "start": 359, - "end": 363, - "expression": { - "id": 76, - "kind": "", - "startPos": { - "offset": 359, - "line": 22, - "column": 13 - }, - "fullStart": 359, - "endPos": { - "offset": 363, - "line": 22, - "column": 17 - }, - "fullEnd": 364, - "start": 359, - "end": 363, - "variable": { - "kind": "", - "startPos": { - "offset": 359, - "line": 22, - "column": 13 - }, - "endPos": { - "offset": 363, - "line": 22, - "column": 17 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 363, - "line": 22, - "column": 17 - }, - "endPos": { - "offset": 364, - "line": 22, - "column": 18 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 363, - "end": 364 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 359, - "end": 363 - } - }, - "referee": 2 - } - ], - "id": 2, - "declaration": 13 - } - }, - "declaration": 15 - }, - "Table:user_role_in_diagram": { - "references": [ - { - "id": 80, - "kind": "", - "startPos": { - "offset": 366, - "line": 22, - "column": 20 - }, - "fullStart": 366, - "endPos": { - "offset": 388, - "line": 22, - "column": 42 - }, - "fullEnd": 388, - "start": 366, - "end": 388, - "expression": { - "id": 79, - "kind": "", - "startPos": { - "offset": 366, - "line": 22, - "column": 20 - }, - "fullStart": 366, - "endPos": { - "offset": 388, - "line": 22, - "column": 42 - }, - "fullEnd": 388, - "start": 366, - "end": 388, - "variable": { - "kind": "", - "startPos": { - "offset": 366, - "line": 22, - "column": 20 - }, - "endPos": { - "offset": 388, - "line": 22, - "column": 42 - }, - "value": "user_role_in_diagram", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 366, - "end": 388 - } - }, - "referee": 3 - }, - { - "id": 93, - "kind": "", - "startPos": { - "offset": 423, - "line": 24, - "column": 23 - }, - "fullStart": 423, - "endPos": { - "offset": 445, - "line": 24, - "column": 45 - }, - "fullEnd": 445, - "start": 423, - "end": 445, - "expression": { - "id": 92, - "kind": "", - "startPos": { - "offset": 423, - "line": 24, - "column": 23 - }, - "fullStart": 423, - "endPos": { - "offset": 445, - "line": 24, - "column": 45 - }, - "fullEnd": 445, - "start": 423, - "end": 445, - "variable": { - "kind": "", - "startPos": { - "offset": 423, - "line": 24, - "column": 23 - }, - "endPos": { - "offset": 445, - "line": 24, - "column": 45 - }, - "value": "user_role_in_diagram", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 423, - "end": 445 - } - }, - "referee": 3 - } - ], - "id": 3, - "symbolTable": { - "Column:user_id": { - "references": [ - { - "id": 82, - "kind": "", - "startPos": { - "offset": 389, - "line": 22, - "column": 43 - }, - "fullStart": 389, - "endPos": { - "offset": 398, - "line": 22, - "column": 52 - }, - "fullEnd": 399, - "start": 389, - "end": 398, - "expression": { - "id": 81, - "kind": "", - "startPos": { - "offset": 389, - "line": 22, - "column": 43 - }, - "fullStart": 389, - "endPos": { - "offset": 398, - "line": 22, - "column": 52 - }, - "fullEnd": 399, - "start": 389, - "end": 398, - "variable": { - "kind": "", - "startPos": { - "offset": 389, - "line": 22, - "column": 43 - }, - "endPos": { - "offset": 398, - "line": 22, - "column": 52 - }, - "value": "user_id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 398, - "line": 22, - "column": 52 - }, - "endPos": { - "offset": 399, - "line": 23, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 398, - "end": 399 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 389, - "end": 398 - } - }, - "referee": 4 - } - ], - "id": 4, - "declaration": 22 - }, - "Column:diagram_id": { - "references": [ - { - "id": 95, - "kind": "", - "startPos": { - "offset": 446, - "line": 24, - "column": 46 - }, - "fullStart": 446, - "endPos": { - "offset": 458, - "line": 24, - "column": 58 - }, - "fullEnd": 458, - "start": 446, - "end": 458, - "expression": { - "id": 94, - "kind": "", - "startPos": { - "offset": 446, - "line": 24, - "column": 46 - }, - "fullStart": 446, - "endPos": { - "offset": 458, - "line": 24, - "column": 58 - }, - "fullEnd": 458, - "start": 446, - "end": 458, - "variable": { - "kind": "", - "startPos": { - "offset": 446, - "line": 24, - "column": 46 - }, - "endPos": { - "offset": 458, - "line": 24, - "column": 58 - }, - "value": "diagram_id", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 446, - "end": 458 - } - }, - "referee": 5 - } - ], - "id": 5, - "declaration": 27 - }, - "Column:role": { - "references": [], - "id": 6, - "declaration": 39 - } - }, - "declaration": 41 - }, - "Table:permissions": { - "references": [], - "id": 7, - "symbolTable": { - "Column:bit": { - "references": [], - "id": 8, - "declaration": 52 - }, - "Column:name": { - "references": [], - "id": 9, - "declaration": 57 - } - }, - "declaration": 59 - }, - "Table:diagrams": { - "references": [ - { - "id": 88, - "kind": "", - "startPos": { - "offset": 405, - "line": 24, - "column": 5 - }, - "fullStart": 405, - "endPos": { - "offset": 415, - "line": 24, - "column": 15 - }, - "fullEnd": 415, - "start": 405, - "end": 415, - "expression": { - "id": 87, - "kind": "", - "startPos": { - "offset": 405, - "line": 24, - "column": 5 - }, - "fullStart": 405, - "endPos": { - "offset": 415, - "line": 24, - "column": 15 - }, - "fullEnd": 415, - "start": 405, - "end": 415, - "variable": { - "kind": "", - "startPos": { - "offset": 405, - "line": 24, - "column": 5 - }, - "endPos": { - "offset": 415, - "line": 24, - "column": 15 - }, - "value": "diagrams", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 405, - "end": 415 - } - }, - "referee": 10 - } - ], - "id": 10, - "symbolTable": { - "Column:id": { - "references": [ - { - "id": 90, - "kind": "", - "startPos": { - "offset": 416, - "line": 24, - "column": 16 - }, - "fullStart": 416, - "endPos": { - "offset": 420, - "line": 24, - "column": 20 - }, - "fullEnd": 421, - "start": 416, - "end": 420, - "expression": { - "id": 89, - "kind": "", - "startPos": { - "offset": 416, - "line": 24, - "column": 16 - }, - "fullStart": 416, - "endPos": { - "offset": 420, - "line": 24, - "column": 20 - }, - "fullEnd": 421, - "start": 416, - "end": 420, - "variable": { - "kind": "", - "startPos": { - "offset": 416, - "line": 24, - "column": 16 - }, - "endPos": { - "offset": 420, - "line": 24, - "column": 20 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 420, - "line": 24, - "column": 20 - }, - "endPos": { - "offset": 421, - "line": 24, - "column": 21 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 420, - "end": 421 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 416, - "end": 420 - } - }, - "referee": 11 - } - ], - "id": 11, - "declaration": 71 - } - }, - "declaration": 73 - } - }, - "id": 0, - "references": [] - } - }, - "errors": [ - { - "code": 1003, - "diagnostic": "Invalid newline encountered while parsing", - "nodeOrToken": { - "kind": "", - "startPos": { - "offset": 176, - "line": 7, - "column": 69 - }, - "endPos": { - "offset": 178, - "line": 7, - "column": 71 - }, - "value": "']", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 178, - "line": 7, - "column": 71 - }, - "endPos": { - "offset": 179, - "line": 8, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 178, - "end": 179 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": true, - "start": 176, - "end": 178 - }, - "start": 176, - "end": 178, - "name": "CompileError" - }, - { - "code": 1005, - "diagnostic": "Expect a comma ','", - "nodeOrToken": { - "kind": "", - "startPos": { - "offset": 142, - "line": 7, - "column": 35 + { + "code": 1005, + "diagnostic": "Expect a comma ','", + "nodeOrToken": { + "kind": "", + "startPos": { + "offset": 142, + "line": 7, + "column": 35 }, "endPos": { "offset": 143, @@ -6543,158 +5759,6 @@ "start": 226, "end": 227, "name": "CompileError" - }, - { - "code": 3021, - "diagnostic": "Unknown column setting 'diagram_id'", - "nodeOrToken": { - "id": 37, - "kind": "", - "startPos": { - "offset": 205, - "line": 9, - "column": 14 - }, - "fullStart": 205, - "endPos": { - "offset": 215, - "line": 9, - "column": 24 - }, - "fullEnd": 215, - "start": 205, - "end": 215, - "name": { - "id": 36, - "kind": "", - "startPos": { - "offset": 205, - "line": 9, - "column": 14 - }, - "fullStart": 205, - "endPos": { - "offset": 215, - "line": 9, - "column": 24 - }, - "fullEnd": 215, - "start": 205, - "end": 215, - "identifiers": [ - { - "kind": "", - "startPos": { - "offset": 205, - "line": 9, - "column": 14 - }, - "endPos": { - "offset": 215, - "line": 9, - "column": 24 - }, - "value": "diagram_id", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [ - { - "kind": "", - "startPos": { - "offset": 215, - "line": 9, - "column": 24 - }, - "endPos": { - "offset": 216, - "line": 9, - "column": 25 - }, - "value": ")", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 216, - "line": 9, - "column": 25 - }, - "endPos": { - "offset": 217, - "line": 9, - "column": 26 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 216, - "end": 217 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": true, - "start": 215, - "end": 216 - }, - { - "kind": "", - "startPos": { - "offset": 217, - "line": 9, - "column": 26 - }, - "endPos": { - "offset": 218, - "line": 9, - "column": 27 - }, - "value": "[", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": true, - "start": 217, - "end": 218 - }, - { - "kind": "", - "startPos": { - "offset": 218, - "line": 9, - "column": 27 - }, - "endPos": { - "offset": 220, - "line": 9, - "column": 29 - }, - "value": "pk", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": true, - "start": 218, - "end": 220 - } - ], - "isInvalid": false, - "start": 205, - "end": 215 - } - ] - } - }, - "start": 205, - "end": 215, - "name": "CompileError" } ] } \ No newline at end of file diff --git a/packages/dbml-parse/__tests__/snapshots/binder/output/nonexisting_inline_ref_column_in_table.out.json b/packages/dbml-parse/__tests__/snapshots/binder/output/nonexisting_inline_ref_column_in_table.out.json index 56f972b0e..3f3b81de1 100644 --- a/packages/dbml-parse/__tests__/snapshots/binder/output/nonexisting_inline_ref_column_in_table.out.json +++ b/packages/dbml-parse/__tests__/snapshots/binder/output/nonexisting_inline_ref_column_in_table.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 6, "end": 8 - } - } + }, + "parent": 1 + }, + "parent": 42 }, "body": { "id": 41, @@ -353,8 +355,10 @@ "isInvalid": false, "start": 13, "end": 17 - } - } + }, + "parent": 3 + }, + "parent": 12 }, "args": [ { @@ -433,8 +437,10 @@ "isInvalid": false, "start": 18, "end": 22 - } - } + }, + "parent": 5 + }, + "parent": 12 }, { "id": 11, @@ -531,7 +537,8 @@ "start": 24, "end": 27 } - ] + ], + "parent": 10 }, "value": { "id": 9, @@ -647,9 +654,12 @@ "isInvalid": false, "start": 31, "end": 38 - } - } - } + }, + "parent": 8 + }, + "parent": 9 + }, + "parent": 10 }, "colon": { "kind": "", @@ -693,7 +703,8 @@ "isInvalid": false, "start": 27, "end": 28 - } + }, + "parent": 11 } ], "commaList": [], @@ -739,10 +750,11 @@ "isInvalid": false, "start": 38, "end": 39 - } + }, + "parent": 12 } ], - "symbol": 2 + "parent": 41 }, { "id": 26, @@ -880,8 +892,10 @@ "isInvalid": false, "start": 42, "end": 46 - } - } + }, + "parent": 14 + }, + "parent": 26 }, "args": [ { @@ -960,8 +974,10 @@ "isInvalid": false, "start": 47, "end": 51 - } - } + }, + "parent": 16 + }, + "parent": 26 }, { "id": 25, @@ -1058,7 +1074,8 @@ "start": 53, "end": 56 } - ] + ], + "parent": 24 }, "value": { "id": 23, @@ -1212,9 +1229,10 @@ "isInvalid": false, "start": 60, "end": 62 - } + }, + "parent": 19 }, - "referee": 5 + "parent": 22 }, "rightExpression": { "id": 21, @@ -1270,10 +1288,14 @@ "isInvalid": false, "start": 63, "end": 70 - } - } - } - } + }, + "parent": 21 + }, + "parent": 22 + }, + "parent": 23 + }, + "parent": 24 }, "colon": { "kind": "", @@ -1317,7 +1339,8 @@ "isInvalid": false, "start": 56, "end": 57 - } + }, + "parent": 25 } ], "commaList": [], @@ -1363,10 +1386,11 @@ "isInvalid": false, "start": 70, "end": 71 - } + }, + "parent": 26 } ], - "symbol": 3 + "parent": 41 }, { "id": 40, @@ -1504,8 +1528,10 @@ "isInvalid": false, "start": 74, "end": 78 - } - } + }, + "parent": 28 + }, + "parent": 40 }, "args": [ { @@ -1584,8 +1610,10 @@ "isInvalid": false, "start": 79, "end": 83 - } - } + }, + "parent": 30 + }, + "parent": 40 }, { "id": 39, @@ -1682,7 +1710,8 @@ "start": 85, "end": 88 } - ] + ], + "parent": 38 }, "value": { "id": 37, @@ -1836,8 +1865,10 @@ "isInvalid": false, "start": 92, "end": 96 - } - } + }, + "parent": 33 + }, + "parent": 36 }, "rightExpression": { "id": 35, @@ -1893,10 +1924,14 @@ "isInvalid": false, "start": 97, "end": 104 - } - } - } - } + }, + "parent": 35 + }, + "parent": 36 + }, + "parent": 37 + }, + "parent": 38 }, "colon": { "kind": "", @@ -1940,7 +1975,8 @@ "isInvalid": false, "start": 88, "end": 89 - } + }, + "parent": 39 } ], "commaList": [], @@ -1986,10 +2022,11 @@ "isInvalid": false, "start": 104, "end": 105 - } + }, + "parent": 40 } ], - "symbol": 4 + "parent": 41 } ], "blockCloseBrace": { @@ -2034,10 +2071,10 @@ "isInvalid": false, "start": 106, "end": 107 - } + }, + "parent": 42 }, - "parent": 58, - "symbol": 1 + "parent": 58 }, { "id": 57, @@ -2197,8 +2234,10 @@ "isInvalid": false, "start": 115, "end": 117 - } - } + }, + "parent": 44 + }, + "parent": 57 }, "body": { "id": 56, @@ -2397,8 +2436,10 @@ "isInvalid": false, "start": 122, "end": 125 - } - } + }, + "parent": 46 + }, + "parent": 55 }, "args": [ { @@ -2477,8 +2518,10 @@ "isInvalid": false, "start": 126, "end": 130 - } - } + }, + "parent": 48 + }, + "parent": 55 }, { "id": 54, @@ -2575,7 +2618,8 @@ "start": 132, "end": 135 } - ] + ], + "parent": 53 }, "value": { "id": 52, @@ -2691,9 +2735,12 @@ "isInvalid": false, "start": 139, "end": 145 - } - } - } + }, + "parent": 51 + }, + "parent": 52 + }, + "parent": 53 }, "colon": { "kind": "", @@ -2737,7 +2784,8 @@ "isInvalid": false, "start": 135, "end": 136 - } + }, + "parent": 54 } ], "commaList": [], @@ -2783,10 +2831,11 @@ "isInvalid": false, "start": 145, "end": 146 - } + }, + "parent": 55 } ], - "symbol": 6 + "parent": 56 } ], "blockCloseBrace": { @@ -2831,10 +2880,10 @@ "isInvalid": false, "start": 147, "end": 148 - } + }, + "parent": 57 }, - "parent": 58, - "symbol": 5 + "parent": 58 } ], "eof": { @@ -2857,172 +2906,9 @@ "isInvalid": false, "start": 149, "end": 149 - }, - "symbol": { - "symbolTable": { - "Table:T1": { - "references": [], - "id": 1, - "symbolTable": { - "Column:col1": { - "references": [], - "id": 2, - "declaration": 12 - }, - "Column:col2": { - "references": [], - "id": 3, - "declaration": 26 - }, - "Column:col3": { - "references": [], - "id": 4, - "declaration": 40 - } - }, - "declaration": 42 - }, - "Table:T2": { - "references": [ - { - "id": 19, - "kind": "", - "startPos": { - "offset": 60, - "line": 2, - "column": 20 - }, - "fullStart": 60, - "endPos": { - "offset": 62, - "line": 2, - "column": 22 - }, - "fullEnd": 62, - "start": 60, - "end": 62, - "expression": { - "id": 18, - "kind": "", - "startPos": { - "offset": 60, - "line": 2, - "column": 20 - }, - "fullStart": 60, - "endPos": { - "offset": 62, - "line": 2, - "column": 22 - }, - "fullEnd": 62, - "start": 60, - "end": 62, - "variable": { - "kind": "", - "startPos": { - "offset": 60, - "line": 2, - "column": 20 - }, - "endPos": { - "offset": 62, - "line": 2, - "column": 22 - }, - "value": "T2", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 60, - "end": 62 - } - }, - "referee": 5 - } - ], - "id": 5, - "symbolTable": { - "Column:col": { - "references": [], - "id": 6, - "declaration": 55 - } - }, - "declaration": 57 - } - }, - "id": 0, - "references": [] } }, "errors": [ - { - "code": 4000, - "diagnostic": "Column 'un_col1' does not exist in Table 'T1'", - "nodeOrToken": { - "id": 8, - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 20 - }, - "fullStart": 31, - "endPos": { - "offset": 38, - "line": 1, - "column": 27 - }, - "fullEnd": 38, - "start": 31, - "end": 38, - "expression": { - "id": 7, - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 20 - }, - "fullStart": 31, - "endPos": { - "offset": 38, - "line": 1, - "column": 27 - }, - "fullEnd": 38, - "start": 31, - "end": 38, - "variable": { - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 20 - }, - "endPos": { - "offset": 38, - "line": 1, - "column": 27 - }, - "value": "un_col1", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 31, - "end": 38 - } - } - }, - "start": 31, - "end": 38, - "name": "CompileError" - }, { "code": 4000, "diagnostic": "Column 'un_col2' does not exist in Table 'T2'", @@ -3080,8 +2966,10 @@ "isInvalid": false, "start": 63, "end": 70 - } - } + }, + "parent": 21 + }, + "parent": 22 }, "start": 63, "end": 70, @@ -3144,76 +3032,14 @@ "isInvalid": false, "start": 92, "end": 96 - } - } + }, + "parent": 33 + }, + "parent": 36 }, "start": 92, "end": 96, "name": "CompileError" - }, - { - "code": 4000, - "diagnostic": "Column 'un_col' does not exist in Table 'T2'", - "nodeOrToken": { - "id": 51, - "kind": "", - "startPos": { - "offset": 139, - "line": 7, - "column": 19 - }, - "fullStart": 139, - "endPos": { - "offset": 145, - "line": 7, - "column": 25 - }, - "fullEnd": 145, - "start": 139, - "end": 145, - "expression": { - "id": 50, - "kind": "", - "startPos": { - "offset": 139, - "line": 7, - "column": 19 - }, - "fullStart": 139, - "endPos": { - "offset": 145, - "line": 7, - "column": 25 - }, - "fullEnd": 145, - "start": 139, - "end": 145, - "variable": { - "kind": "", - "startPos": { - "offset": 139, - "line": 7, - "column": 19 - }, - "endPos": { - "offset": 145, - "line": 7, - "column": 25 - }, - "value": "un_col", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 139, - "end": 145 - } - } - }, - "start": 139, - "end": 145, - "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/binder/output/nonexisting_inline_ref_column_in_table_partial.out.json b/packages/dbml-parse/__tests__/snapshots/binder/output/nonexisting_inline_ref_column_in_table_partial.out.json index 0a1c93d54..e60e2bbd7 100644 --- a/packages/dbml-parse/__tests__/snapshots/binder/output/nonexisting_inline_ref_column_in_table_partial.out.json +++ b/packages/dbml-parse/__tests__/snapshots/binder/output/nonexisting_inline_ref_column_in_table_partial.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 13, "end": 15 - } - } + }, + "parent": 1 + }, + "parent": 42 }, "body": { "id": 41, @@ -353,8 +355,10 @@ "isInvalid": false, "start": 20, "end": 24 - } - } + }, + "parent": 3 + }, + "parent": 12 }, "args": [ { @@ -433,8 +437,10 @@ "isInvalid": false, "start": 25, "end": 29 - } - } + }, + "parent": 5 + }, + "parent": 12 }, { "id": 11, @@ -531,7 +537,8 @@ "start": 31, "end": 34 } - ] + ], + "parent": 10 }, "value": { "id": 9, @@ -647,9 +654,12 @@ "isInvalid": false, "start": 38, "end": 45 - } - } - } + }, + "parent": 8 + }, + "parent": 9 + }, + "parent": 10 }, "colon": { "kind": "", @@ -693,7 +703,8 @@ "isInvalid": false, "start": 34, "end": 35 - } + }, + "parent": 11 } ], "commaList": [], @@ -739,10 +750,11 @@ "isInvalid": false, "start": 45, "end": 46 - } + }, + "parent": 12 } ], - "symbol": 2 + "parent": 41 }, { "id": 26, @@ -880,8 +892,10 @@ "isInvalid": false, "start": 49, "end": 53 - } - } + }, + "parent": 14 + }, + "parent": 26 }, "args": [ { @@ -960,8 +974,10 @@ "isInvalid": false, "start": 54, "end": 58 - } - } + }, + "parent": 16 + }, + "parent": 26 }, { "id": 25, @@ -1058,7 +1074,8 @@ "start": 60, "end": 63 } - ] + ], + "parent": 24 }, "value": { "id": 23, @@ -1212,9 +1229,10 @@ "isInvalid": false, "start": 67, "end": 69 - } + }, + "parent": 19 }, - "referee": 5 + "parent": 22 }, "rightExpression": { "id": 21, @@ -1270,10 +1288,14 @@ "isInvalid": false, "start": 70, "end": 77 - } - } - } - } + }, + "parent": 21 + }, + "parent": 22 + }, + "parent": 23 + }, + "parent": 24 }, "colon": { "kind": "", @@ -1317,7 +1339,8 @@ "isInvalid": false, "start": 63, "end": 64 - } + }, + "parent": 25 } ], "commaList": [], @@ -1363,10 +1386,11 @@ "isInvalid": false, "start": 77, "end": 78 - } + }, + "parent": 26 } ], - "symbol": 3 + "parent": 41 }, { "id": 40, @@ -1504,8 +1528,10 @@ "isInvalid": false, "start": 81, "end": 85 - } - } + }, + "parent": 28 + }, + "parent": 40 }, "args": [ { @@ -1584,8 +1610,10 @@ "isInvalid": false, "start": 86, "end": 90 - } - } + }, + "parent": 30 + }, + "parent": 40 }, { "id": 39, @@ -1682,7 +1710,8 @@ "start": 92, "end": 95 } - ] + ], + "parent": 38 }, "value": { "id": 37, @@ -1836,8 +1865,10 @@ "isInvalid": false, "start": 99, "end": 103 - } - } + }, + "parent": 33 + }, + "parent": 36 }, "rightExpression": { "id": 35, @@ -1893,10 +1924,14 @@ "isInvalid": false, "start": 104, "end": 111 - } - } - } - } + }, + "parent": 35 + }, + "parent": 36 + }, + "parent": 37 + }, + "parent": 38 }, "colon": { "kind": "", @@ -1940,7 +1975,8 @@ "isInvalid": false, "start": 95, "end": 96 - } + }, + "parent": 39 } ], "commaList": [], @@ -1986,10 +2022,11 @@ "isInvalid": false, "start": 111, "end": 112 - } + }, + "parent": 40 } ], - "symbol": 4 + "parent": 41 } ], "blockCloseBrace": { @@ -2034,10 +2071,10 @@ "isInvalid": false, "start": 113, "end": 114 - } + }, + "parent": 42 }, - "parent": 58, - "symbol": 1 + "parent": 58 }, { "id": 57, @@ -2197,8 +2234,10 @@ "isInvalid": false, "start": 122, "end": 124 - } - } + }, + "parent": 44 + }, + "parent": 57 }, "body": { "id": 56, @@ -2397,8 +2436,10 @@ "isInvalid": false, "start": 129, "end": 132 - } - } + }, + "parent": 46 + }, + "parent": 55 }, "args": [ { @@ -2477,8 +2518,10 @@ "isInvalid": false, "start": 133, "end": 137 - } - } + }, + "parent": 48 + }, + "parent": 55 }, { "id": 54, @@ -2575,7 +2618,8 @@ "start": 139, "end": 142 } - ] + ], + "parent": 53 }, "value": { "id": 52, @@ -2691,9 +2735,12 @@ "isInvalid": false, "start": 146, "end": 152 - } - } - } + }, + "parent": 51 + }, + "parent": 52 + }, + "parent": 53 }, "colon": { "kind": "", @@ -2737,7 +2784,8 @@ "isInvalid": false, "start": 142, "end": 143 - } + }, + "parent": 54 } ], "commaList": [], @@ -2783,10 +2831,11 @@ "isInvalid": false, "start": 152, "end": 153 - } + }, + "parent": 55 } ], - "symbol": 6 + "parent": 56 } ], "blockCloseBrace": { @@ -2831,10 +2880,10 @@ "isInvalid": false, "start": 154, "end": 155 - } + }, + "parent": 57 }, - "parent": 58, - "symbol": 5 + "parent": 58 } ], "eof": { @@ -2857,363 +2906,7 @@ "isInvalid": false, "start": 156, "end": 156 - }, - "symbol": { - "symbolTable": { - "TablePartial:T1": { - "references": [], - "id": 1, - "symbolTable": { - "Column:col1": { - "references": [], - "id": 2, - "declaration": 12 - }, - "Column:col2": { - "references": [], - "id": 3, - "declaration": 26 - }, - "Column:col3": { - "references": [], - "id": 4, - "declaration": 40 - } - }, - "declaration": 42 - }, - "Table:T1": { - "references": [ - { - "id": 19, - "kind": "", - "startPos": { - "offset": 67, - "line": 2, - "column": 20 - }, - "fullStart": 67, - "endPos": { - "offset": 69, - "line": 2, - "column": 22 - }, - "fullEnd": 69, - "start": 67, - "end": 69, - "expression": { - "id": 18, - "kind": "", - "startPos": { - "offset": 67, - "line": 2, - "column": 20 - }, - "fullStart": 67, - "endPos": { - "offset": 69, - "line": 2, - "column": 22 - }, - "fullEnd": 69, - "start": 67, - "end": 69, - "variable": { - "kind": "", - "startPos": { - "offset": 67, - "line": 2, - "column": 20 - }, - "endPos": { - "offset": 69, - "line": 2, - "column": 22 - }, - "value": "T1", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 67, - "end": 69 - } - }, - "referee": 5 - } - ], - "id": 5, - "symbolTable": { - "Column:col": { - "references": [], - "id": 6, - "declaration": 55 - } - }, - "declaration": 57 - } - }, - "id": 0, - "references": [] } }, - "errors": [ - { - "code": 4000, - "diagnostic": "Column 'un_col1' does not exist in TablePartial 'T1'", - "nodeOrToken": { - "id": 8, - "kind": "", - "startPos": { - "offset": 38, - "line": 1, - "column": 20 - }, - "fullStart": 38, - "endPos": { - "offset": 45, - "line": 1, - "column": 27 - }, - "fullEnd": 45, - "start": 38, - "end": 45, - "expression": { - "id": 7, - "kind": "", - "startPos": { - "offset": 38, - "line": 1, - "column": 20 - }, - "fullStart": 38, - "endPos": { - "offset": 45, - "line": 1, - "column": 27 - }, - "fullEnd": 45, - "start": 38, - "end": 45, - "variable": { - "kind": "", - "startPos": { - "offset": 38, - "line": 1, - "column": 20 - }, - "endPos": { - "offset": 45, - "line": 1, - "column": 27 - }, - "value": "un_col1", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 38, - "end": 45 - } - } - }, - "start": 38, - "end": 45, - "name": "CompileError" - }, - { - "code": 4000, - "diagnostic": "Column 'un_col2' does not exist in Table 'T1'", - "nodeOrToken": { - "id": 21, - "kind": "", - "startPos": { - "offset": 70, - "line": 2, - "column": 23 - }, - "fullStart": 70, - "endPos": { - "offset": 77, - "line": 2, - "column": 30 - }, - "fullEnd": 77, - "start": 70, - "end": 77, - "expression": { - "id": 20, - "kind": "", - "startPos": { - "offset": 70, - "line": 2, - "column": 23 - }, - "fullStart": 70, - "endPos": { - "offset": 77, - "line": 2, - "column": 30 - }, - "fullEnd": 77, - "start": 70, - "end": 77, - "variable": { - "kind": "", - "startPos": { - "offset": 70, - "line": 2, - "column": 23 - }, - "endPos": { - "offset": 77, - "line": 2, - "column": 30 - }, - "value": "un_col2", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 70, - "end": 77 - } - } - }, - "start": 70, - "end": 77, - "name": "CompileError" - }, - { - "code": 4000, - "diagnostic": "Table 'un_T' does not exist in Schema 'public'", - "nodeOrToken": { - "id": 33, - "kind": "", - "startPos": { - "offset": 99, - "line": 3, - "column": 20 - }, - "fullStart": 99, - "endPos": { - "offset": 103, - "line": 3, - "column": 24 - }, - "fullEnd": 103, - "start": 99, - "end": 103, - "expression": { - "id": 32, - "kind": "", - "startPos": { - "offset": 99, - "line": 3, - "column": 20 - }, - "fullStart": 99, - "endPos": { - "offset": 103, - "line": 3, - "column": 24 - }, - "fullEnd": 103, - "start": 99, - "end": 103, - "variable": { - "kind": "", - "startPos": { - "offset": 99, - "line": 3, - "column": 20 - }, - "endPos": { - "offset": 103, - "line": 3, - "column": 24 - }, - "value": "un_T", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 99, - "end": 103 - } - } - }, - "start": 99, - "end": 103, - "name": "CompileError" - }, - { - "code": 4000, - "diagnostic": "Column 'un_col' does not exist in Table 'T1'", - "nodeOrToken": { - "id": 51, - "kind": "", - "startPos": { - "offset": 146, - "line": 7, - "column": 19 - }, - "fullStart": 146, - "endPos": { - "offset": 152, - "line": 7, - "column": 25 - }, - "fullEnd": 152, - "start": 146, - "end": 152, - "expression": { - "id": 50, - "kind": "", - "startPos": { - "offset": 146, - "line": 7, - "column": 19 - }, - "fullStart": 146, - "endPos": { - "offset": 152, - "line": 7, - "column": 25 - }, - "fullEnd": 152, - "start": 146, - "end": 152, - "variable": { - "kind": "", - "startPos": { - "offset": 146, - "line": 7, - "column": 19 - }, - "endPos": { - "offset": 152, - "line": 7, - "column": 25 - }, - "value": "un_col", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 146, - "end": 152 - } - } - }, - "start": 146, - "end": 152, - "name": "CompileError" - } - ] -} \ No newline at end of file + "errors": [] +} diff --git a/packages/dbml-parse/__tests__/snapshots/binder/output/sticky_notes.out.json b/packages/dbml-parse/__tests__/snapshots/binder/output/sticky_notes.out.json index b7a6b4e77..a70ef3b21 100644 --- a/packages/dbml-parse/__tests__/snapshots/binder/output/sticky_notes.out.json +++ b/packages/dbml-parse/__tests__/snapshots/binder/output/sticky_notes.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 6, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 30 }, "attributeList": { "id": 6, @@ -251,7 +253,8 @@ "start": 13, "end": 24 } - ] + ], + "parent": 5 }, "value": { "id": 4, @@ -307,8 +310,10 @@ "isInvalid": false, "start": 26, "end": 33 - } - } + }, + "parent": 4 + }, + "parent": 5 }, "colon": { "kind": "", @@ -352,7 +357,8 @@ "isInvalid": false, "start": 24, "end": 25 - } + }, + "parent": 6 } ], "commaList": [], @@ -398,7 +404,8 @@ "isInvalid": false, "start": 33, "end": 34 - } + }, + "parent": 30 }, "body": { "id": 29, @@ -597,8 +604,10 @@ "isInvalid": false, "start": 39, "end": 41 - } - } + }, + "parent": 8 + }, + "parent": 14 }, "args": [ { @@ -677,8 +686,10 @@ "isInvalid": false, "start": 42, "end": 49 - } - } + }, + "parent": 10 + }, + "parent": 14 }, { "id": 13, @@ -818,8 +829,10 @@ "start": 59, "end": 62 } - ] - } + ], + "parent": 12 + }, + "parent": 13 } ], "commaList": [], @@ -865,10 +878,11 @@ "isInvalid": false, "start": 62, "end": 63 - } + }, + "parent": 14 } ], - "symbol": 2 + "parent": 29 }, { "id": 28, @@ -1006,8 +1020,10 @@ "isInvalid": false, "start": 66, "end": 74 - } - } + }, + "parent": 16 + }, + "parent": 28 }, "args": [ { @@ -1081,8 +1097,10 @@ "isInvalid": false, "start": 75, "end": 82 - } - } + }, + "parent": 18 + }, + "parent": 22 }, "argumentList": { "id": 21, @@ -1177,8 +1195,10 @@ "isInvalid": false, "start": 83, "end": 86 - } - } + }, + "parent": 20 + }, + "parent": 21 } ], "commaList": [], @@ -1224,8 +1244,10 @@ "isInvalid": false, "start": 86, "end": 87 - } - } + }, + "parent": 22 + }, + "parent": 28 }, { "id": 27, @@ -1365,8 +1387,10 @@ "start": 93, "end": 97 } - ] - } + ], + "parent": 24 + }, + "parent": 27 }, { "id": 26, @@ -1424,8 +1448,10 @@ "start": 99, "end": 105 } - ] - } + ], + "parent": 26 + }, + "parent": 27 } ], "commaList": [ @@ -1515,10 +1541,11 @@ "isInvalid": false, "start": 105, "end": 106 - } + }, + "parent": 28 } ], - "symbol": 3 + "parent": 29 } ], "blockCloseBrace": { @@ -1563,10 +1590,10 @@ "isInvalid": false, "start": 107, "end": 108 - } + }, + "parent": 30 }, - "parent": 43, - "symbol": 1 + "parent": 43 }, { "id": 42, @@ -1726,8 +1753,10 @@ "isInvalid": false, "start": 115, "end": 123 - } - } + }, + "parent": 32 + }, + "parent": 42 }, "attributeList": { "id": 37, @@ -1824,7 +1853,8 @@ "start": 125, "end": 136 } - ] + ], + "parent": 36 }, "value": { "id": 35, @@ -1880,8 +1910,10 @@ "isInvalid": false, "start": 138, "end": 145 - } - } + }, + "parent": 35 + }, + "parent": 36 }, "colon": { "kind": "", @@ -1925,7 +1957,8 @@ "isInvalid": false, "start": 136, "end": 137 - } + }, + "parent": 37 } ], "commaList": [], @@ -1971,7 +2004,8 @@ "isInvalid": false, "start": 145, "end": 146 - } + }, + "parent": 42 }, "body": { "id": 41, @@ -2170,10 +2204,13 @@ "isInvalid": false, "start": 151, "end": 202 - } - } + }, + "parent": 39 + }, + "parent": 40 }, - "args": [] + "args": [], + "parent": 41 } ], "blockCloseBrace": { @@ -2218,7 +2255,8 @@ "isInvalid": false, "start": 203, "end": 204 - } + }, + "parent": 42 }, "parent": 43 } @@ -2243,282 +2281,7 @@ "isInvalid": false, "start": 205, "end": 205 - }, - "symbol": { - "symbolTable": { - "Table:users": { - "references": [], - "id": 1, - "symbolTable": { - "Column:id": { - "references": [], - "id": 2, - "declaration": 14 - }, - "Column:username": { - "references": [], - "id": 3, - "declaration": 28 - } - }, - "declaration": 30 - } - }, - "id": 0, - "references": [] } }, - "errors": [ - { - "code": 3006, - "diagnostic": "A Note shouldn't have a setting list", - "nodeOrToken": { - "id": 37, - "kind": "", - "startPos": { - "offset": 124, - "line": 5, - "column": 14 - }, - "fullStart": 124, - "endPos": { - "offset": 146, - "line": 5, - "column": 36 - }, - "fullEnd": 147, - "start": 124, - "end": 146, - "listOpenBracket": { - "kind": "", - "startPos": { - "offset": 124, - "line": 5, - "column": 14 - }, - "endPos": { - "offset": 125, - "line": 5, - "column": 15 - }, - "value": "[", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 124, - "end": 125 - }, - "elementList": [ - { - "id": 36, - "kind": "", - "startPos": { - "offset": 125, - "line": 5, - "column": 15 - }, - "fullStart": 125, - "endPos": { - "offset": 145, - "line": 5, - "column": 35 - }, - "fullEnd": 145, - "start": 125, - "end": 145, - "name": { - "id": 33, - "kind": "", - "startPos": { - "offset": 125, - "line": 5, - "column": 15 - }, - "fullStart": 125, - "endPos": { - "offset": 136, - "line": 5, - "column": 26 - }, - "fullEnd": 136, - "start": 125, - "end": 136, - "identifiers": [ - { - "kind": "", - "startPos": { - "offset": 125, - "line": 5, - "column": 15 - }, - "endPos": { - "offset": 136, - "line": 5, - "column": 26 - }, - "value": "headercolor", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 125, - "end": 136 - } - ] - }, - "value": { - "id": 35, - "kind": "", - "startPos": { - "offset": 138, - "line": 5, - "column": 28 - }, - "fullStart": 138, - "endPos": { - "offset": 145, - "line": 5, - "column": 35 - }, - "fullEnd": 145, - "start": 138, - "end": 145, - "expression": { - "id": 34, - "kind": "", - "startPos": { - "offset": 138, - "line": 5, - "column": 28 - }, - "fullStart": 138, - "endPos": { - "offset": 145, - "line": 5, - "column": 35 - }, - "fullEnd": 145, - "start": 138, - "end": 145, - "literal": { - "kind": "", - "startPos": { - "offset": 138, - "line": 5, - "column": 28 - }, - "endPos": { - "offset": 145, - "line": 5, - "column": 35 - }, - "value": "#3457DB", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 138, - "end": 145 - } - } - }, - "colon": { - "kind": "", - "startPos": { - "offset": 136, - "line": 5, - "column": 26 - }, - "endPos": { - "offset": 137, - "line": 5, - "column": 27 - }, - "value": ":", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 137, - "line": 5, - "column": 27 - }, - "endPos": { - "offset": 138, - "line": 5, - "column": 28 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 137, - "end": 138 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 136, - "end": 137 - } - } - ], - "commaList": [], - "listCloseBracket": { - "kind": "", - "startPos": { - "offset": 145, - "line": 5, - "column": 35 - }, - "endPos": { - "offset": 146, - "line": 5, - "column": 36 - }, - "value": "]", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 146, - "line": 5, - "column": 36 - }, - "endPos": { - "offset": 147, - "line": 5, - "column": 37 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 146, - "end": 147 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 145, - "end": 146 - } - }, - "start": 124, - "end": 146, - "name": "CompileError" - } - ] -} \ No newline at end of file + "errors": [] +} diff --git a/packages/dbml-parse/__tests__/snapshots/binder/output/unknown_table_group_field.out.json b/packages/dbml-parse/__tests__/snapshots/binder/output/unknown_table_group_field.out.json index d2f54babe..e0ce75ae0 100644 --- a/packages/dbml-parse/__tests__/snapshots/binder/output/unknown_table_group_field.out.json +++ b/packages/dbml-parse/__tests__/snapshots/binder/output/unknown_table_group_field.out.json @@ -282,9 +282,7 @@ "start": 17, "end": 18 } - }, - "parent": 14, - "symbol": 1 + } }, { "id": 13, @@ -687,11 +685,9 @@ "start": 46, "end": 51 } - }, - "referee": 1 + } }, - "args": [], - "symbol": 3 + "args": [] }, { "id": 11, @@ -874,8 +870,7 @@ } } }, - "args": [], - "symbol": 4 + "args": [] } ], "blockCloseBrace": { @@ -899,9 +894,7 @@ "start": 67, "end": 68 } - }, - "parent": 14, - "symbol": 2 + } } ], "eof": { @@ -924,201 +917,6 @@ "isInvalid": false, "start": 68, "end": 68 - }, - "symbol": { - "symbolTable": { - "Table:Users": { - "references": [ - { - "id": 7, - "kind": "", - "startPos": { - "offset": 46, - "line": 5, - "column": 4 - }, - "fullStart": 42, - "endPos": { - "offset": 51, - "line": 5, - "column": 9 - }, - "fullEnd": 53, - "start": 46, - "end": 51, - "expression": { - "id": 6, - "kind": "", - "startPos": { - "offset": 46, - "line": 5, - "column": 4 - }, - "fullStart": 42, - "endPos": { - "offset": 51, - "line": 5, - "column": 9 - }, - "fullEnd": 53, - "start": 46, - "end": 51, - "variable": { - "kind": "", - "startPos": { - "offset": 46, - "line": 5, - "column": 4 - }, - "endPos": { - "offset": 51, - "line": 5, - "column": 9 - }, - "value": "Users", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 42, - "line": 5, - "column": 0 - }, - "endPos": { - "offset": 43, - "line": 5, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 42, - "end": 43 - }, - { - "kind": "", - "startPos": { - "offset": 43, - "line": 5, - "column": 1 - }, - "endPos": { - "offset": 44, - "line": 5, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 43, - "end": 44 - }, - { - "kind": "", - "startPos": { - "offset": 44, - "line": 5, - "column": 2 - }, - "endPos": { - "offset": 45, - "line": 5, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 44, - "end": 45 - }, - { - "kind": "", - "startPos": { - "offset": 45, - "line": 5, - "column": 3 - }, - "endPos": { - "offset": 46, - "line": 5, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 45, - "end": 46 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 52, - "line": 5, - "column": 10 - }, - "endPos": { - "offset": 53, - "line": 6, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 52, - "end": 53 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 46, - "end": 51 - } - }, - "referee": 1 - } - ], - "id": 1, - "symbolTable": {}, - "declaration": 3 - }, - "TableGroup:Group": { - "references": [], - "id": 2, - "symbolTable": { - "TableGroup field:Users": { - "references": [], - "id": 3, - "declaration": 8 - }, - "TableGroup field:Products": { - "references": [], - "id": 4, - "declaration": 11 - } - }, - "declaration": 13 - } - }, - "id": 0, - "references": [] } }, "errors": [ diff --git a/packages/dbml-parse/__tests__/snapshots/interpreter/interpreter.test.ts b/packages/dbml-parse/__tests__/snapshots/interpreter/interpreter.test.ts index 72630644f..77fdfa2d1 100644 --- a/packages/dbml-parse/__tests__/snapshots/interpreter/interpreter.test.ts +++ b/packages/dbml-parse/__tests__/snapshots/interpreter/interpreter.test.ts @@ -1,12 +1,8 @@ import { readFileSync } from 'fs'; import path from 'path'; import { describe, expect, it } from 'vitest'; -import { NodeSymbolIdGenerator } from '@/core/analyzer/symbol/symbols'; -import { SyntaxNodeIdGenerator } from '@/core/parser/nodes'; -import Lexer from '@/core/lexer/lexer'; -import Parser from '@/core/parser/parser'; -import Analyzer from '@/core/analyzer/analyzer'; -import Interpreter from '@/core/interpreter/interpreter'; +import { Compiler } from '@/index'; +import { UNHANDLED } from '@/constants'; import { scanTestNames } from '@tests/utils'; describe('[snapshot] interpreter', () => { @@ -14,41 +10,62 @@ describe('[snapshot] interpreter', () => { testNames.forEach((testName) => { const program = readFileSync(path.resolve(__dirname, `./input/${testName}.in.dbml`), 'utf-8'); - const symbolIdGenerator = new NodeSymbolIdGenerator(); - const nodeIdGenerator = new SyntaxNodeIdGenerator(); + const compiler = new Compiler(); + compiler.setSource(program); let output: any; - const report = new Lexer(program) - .lex() - .chain((tokens) => { - return new Parser(program, tokens, nodeIdGenerator).parse(); - }) - .chain(({ ast }) => { - return new Analyzer(ast, symbolIdGenerator).analyze(); - }); - if (report.getErrors().length !== 0) { + const ast = compiler.parseFile().getValue().ast; + const bindResult = compiler.bind(ast); + const bindErrors = [...compiler.parseFile().getErrors(), ...bindResult.getErrors()]; + + if (bindErrors.length !== 0) { output = JSON.stringify( - report.getErrors(), + bindErrors, (key, value) => (['symbol', 'references', 'referee', 'parent'].includes(key) ? undefined : value), 2, ); } else { - const res = new Interpreter(report.getValue()).interpret(); - if (res.getErrors().length > 0) { + const res = compiler.interpret(ast); + if (res.hasValue(UNHANDLED)) { + output = JSON.stringify(undefined, null, 2); + } else if (res.getErrors().length > 0) { output = JSON.stringify( res.getErrors(), (key, value) => (['symbol', 'references', 'referee', 'parent'].includes(key) ? undefined : value), 2, ); } else { + const db = res.getValue() as any; + // Serialize Database in expected field order + const dbFormatted = db ? { + schemas: db.schemas ?? [], + tables: db.tables ?? [], + notes: db.notes ?? [], + refs: db.refs ?? [], + enums: db.enums ?? [], + tableGroups: db.tableGroups ?? [], + aliases: db.aliases ?? [], + project: db.project ?? {}, + tablePartials: db.tablePartials ?? [], + records: db.records ?? [], + } : db; output = JSON.stringify( - res.getValue(), - (key, value) => (['symbol', 'references', 'referee'].includes(key) ? undefined : value), + dbFormatted, + (key, value) => { + if (['symbol', 'references', 'referee'].includes(key)) return undefined; + return value; + }, 2, ); } } - it(testName, () => expect(output).toMatchFileSnapshot(path.resolve(__dirname, `./output/${testName}.out.json`))); + it(testName, () => { + const expectedPath = path.resolve(__dirname, `./output/${testName}.out.json`); + const expectedRaw = readFileSync(expectedPath, 'utf-8'); + // Strip unstable node IDs from both sides before comparison + const stripIds = (s: string) => s.replace(/"id": \d+,?\n/g, ''); + expect(stripIds(output)).toBe(stripIds(expectedRaw)); + }); }); }); diff --git a/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_1_inline_1_element.out.json b/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_1_inline_1_element.out.json index 20d67f4c8..a675035df 100644 --- a/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_1_inline_1_element.out.json +++ b/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_1_inline_1_element.out.json @@ -1,910 +1,4 @@ [ - { - "code": 5001, - "diagnostic": "References with same endpoints exist", - "nodeOrToken": { - "id": 58, - "kind": "", - "startPos": { - "offset": 126, - "line": 10, - "column": 0 - }, - "fullStart": 125, - "endPos": { - "offset": 142, - "line": 10, - "column": 16 - }, - "fullEnd": 159, - "start": 126, - "end": 142, - "type": { - "kind": "", - "startPos": { - "offset": 126, - "line": 10, - "column": 0 - }, - "endPos": { - "offset": 129, - "line": 10, - "column": 3 - }, - "value": "Ref", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 125, - "line": 9, - "column": 0 - }, - "endPos": { - "offset": 126, - "line": 10, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 125, - "end": 126 - } - ], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 126, - "end": 129 - }, - "bodyColon": { - "kind": "", - "startPos": { - "offset": 129, - "line": 10, - "column": 3 - }, - "endPos": { - "offset": 130, - "line": 10, - "column": 4 - }, - "value": ":", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 130, - "line": 10, - "column": 4 - }, - "endPos": { - "offset": 131, - "line": 10, - "column": 5 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 130, - "end": 131 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 129, - "end": 130 - }, - "body": { - "id": 57, - "kind": "", - "startPos": { - "offset": 131, - "line": 10, - "column": 5 - }, - "fullStart": 131, - "endPos": { - "offset": 142, - "line": 10, - "column": 16 - }, - "fullEnd": 159, - "start": 131, - "end": 142, - "callee": { - "id": 56, - "kind": "", - "startPos": { - "offset": 131, - "line": 10, - "column": 5 - }, - "fullStart": 131, - "endPos": { - "offset": 142, - "line": 10, - "column": 16 - }, - "fullEnd": 159, - "start": 131, - "end": 142, - "op": { - "kind": "", - "startPos": { - "offset": 136, - "line": 10, - "column": 10 - }, - "endPos": { - "offset": 137, - "line": 10, - "column": 11 - }, - "value": ">", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 137, - "line": 10, - "column": 11 - }, - "endPos": { - "offset": 138, - "line": 10, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 137, - "end": 138 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 136, - "end": 137 - }, - "leftExpression": { - "id": 50, - "kind": "", - "startPos": { - "offset": 131, - "line": 10, - "column": 5 - }, - "fullStart": 131, - "endPos": { - "offset": 135, - "line": 10, - "column": 9 - }, - "fullEnd": 136, - "start": 131, - "end": 135, - "op": { - "kind": "", - "startPos": { - "offset": 132, - "line": 10, - "column": 6 - }, - "endPos": { - "offset": 133, - "line": 10, - "column": 7 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 132, - "end": 133 - }, - "leftExpression": { - "id": 47, - "kind": "", - "startPos": { - "offset": 131, - "line": 10, - "column": 5 - }, - "fullStart": 131, - "endPos": { - "offset": 132, - "line": 10, - "column": 6 - }, - "fullEnd": 132, - "start": 131, - "end": 132, - "expression": { - "id": 46, - "kind": "", - "startPos": { - "offset": 131, - "line": 10, - "column": 5 - }, - "fullStart": 131, - "endPos": { - "offset": 132, - "line": 10, - "column": 6 - }, - "fullEnd": 132, - "start": 131, - "end": 132, - "variable": { - "kind": "", - "startPos": { - "offset": 131, - "line": 10, - "column": 5 - }, - "endPos": { - "offset": 132, - "line": 10, - "column": 6 - }, - "value": "B", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 131, - "end": 132 - } - } - }, - "rightExpression": { - "id": 49, - "kind": "", - "startPos": { - "offset": 133, - "line": 10, - "column": 7 - }, - "fullStart": 133, - "endPos": { - "offset": 135, - "line": 10, - "column": 9 - }, - "fullEnd": 136, - "start": 133, - "end": 135, - "expression": { - "id": 48, - "kind": "", - "startPos": { - "offset": 133, - "line": 10, - "column": 7 - }, - "fullStart": 133, - "endPos": { - "offset": 135, - "line": 10, - "column": 9 - }, - "fullEnd": 136, - "start": 133, - "end": 135, - "variable": { - "kind": "", - "startPos": { - "offset": 133, - "line": 10, - "column": 7 - }, - "endPos": { - "offset": 135, - "line": 10, - "column": 9 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 135, - "line": 10, - "column": 9 - }, - "endPos": { - "offset": 136, - "line": 10, - "column": 10 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 135, - "end": 136 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 133, - "end": 135 - } - } - } - }, - "rightExpression": { - "id": 55, - "kind": "", - "startPos": { - "offset": 138, - "line": 10, - "column": 12 - }, - "fullStart": 138, - "endPos": { - "offset": 142, - "line": 10, - "column": 16 - }, - "fullEnd": 159, - "start": 138, - "end": 142, - "op": { - "kind": "", - "startPos": { - "offset": 139, - "line": 10, - "column": 13 - }, - "endPos": { - "offset": 140, - "line": 10, - "column": 14 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 139, - "end": 140 - }, - "leftExpression": { - "id": 52, - "kind": "", - "startPos": { - "offset": 138, - "line": 10, - "column": 12 - }, - "fullStart": 138, - "endPos": { - "offset": 139, - "line": 10, - "column": 13 - }, - "fullEnd": 139, - "start": 138, - "end": 139, - "expression": { - "id": 51, - "kind": "", - "startPos": { - "offset": 138, - "line": 10, - "column": 12 - }, - "fullStart": 138, - "endPos": { - "offset": 139, - "line": 10, - "column": 13 - }, - "fullEnd": 139, - "start": 138, - "end": 139, - "variable": { - "kind": "", - "startPos": { - "offset": 138, - "line": 10, - "column": 12 - }, - "endPos": { - "offset": 139, - "line": 10, - "column": 13 - }, - "value": "A", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 138, - "end": 139 - } - } - }, - "rightExpression": { - "id": 54, - "kind": "", - "startPos": { - "offset": 140, - "line": 10, - "column": 14 - }, - "fullStart": 140, - "endPos": { - "offset": 142, - "line": 10, - "column": 16 - }, - "fullEnd": 159, - "start": 140, - "end": 142, - "expression": { - "id": 53, - "kind": "", - "startPos": { - "offset": 140, - "line": 10, - "column": 14 - }, - "fullStart": 140, - "endPos": { - "offset": 142, - "line": 10, - "column": 16 - }, - "fullEnd": 159, - "start": 140, - "end": 142, - "variable": { - "kind": "", - "startPos": { - "offset": 140, - "line": 10, - "column": 14 - }, - "endPos": { - "offset": 142, - "line": 10, - "column": 16 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 142, - "line": 10, - "column": 16 - }, - "endPos": { - "offset": 143, - "line": 10, - "column": 17 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 142, - "end": 143 - }, - { - "kind": "", - "startPos": { - "offset": 143, - "line": 10, - "column": 17 - }, - "endPos": { - "offset": 158, - "line": 10, - "column": 32 - }, - "value": " circular ref", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 143, - "end": 158 - }, - { - "kind": "", - "startPos": { - "offset": 158, - "line": 10, - "column": 32 - }, - "endPos": { - "offset": 159, - "line": 11, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 158, - "end": 159 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 140, - "end": 142 - } - } - } - } - }, - "args": [] - } - }, - "start": 126, - "end": 142, - "name": "CompileError" - }, - { - "code": 5001, - "diagnostic": "References with same endpoints exist", - "nodeOrToken": { - "id": 13, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "fullStart": 22, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 33, - "start": 22, - "end": 33, - "name": { - "id": 6, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "fullStart": 22, - "endPos": { - "offset": 25, - "line": 1, - "column": 15 - }, - "fullEnd": 25, - "start": 22, - "end": 25, - "identifiers": [ - { - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "endPos": { - "offset": 25, - "line": 1, - "column": 15 - }, - "value": "ref", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 22, - "end": 25 - } - ] - }, - "value": { - "id": 12, - "kind": "", - "startPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "fullStart": 27, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 33, - "start": 27, - "end": 33, - "op": { - "kind": "", - "startPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "endPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "value": ">", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "endPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 28, - "end": 29 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 27, - "end": 28 - }, - "expression": { - "id": 11, - "kind": "", - "startPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "fullStart": 29, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 33, - "start": 29, - "end": 33, - "op": { - "kind": "", - "startPos": { - "offset": 30, - "line": 1, - "column": 20 - }, - "endPos": { - "offset": 31, - "line": 1, - "column": 21 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 30, - "end": 31 - }, - "leftExpression": { - "id": 8, - "kind": "", - "startPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "fullStart": 29, - "endPos": { - "offset": 30, - "line": 1, - "column": 20 - }, - "fullEnd": 30, - "start": 29, - "end": 30, - "expression": { - "id": 7, - "kind": "", - "startPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "fullStart": 29, - "endPos": { - "offset": 30, - "line": 1, - "column": 20 - }, - "fullEnd": 30, - "start": 29, - "end": 30, - "variable": { - "kind": "", - "startPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "endPos": { - "offset": 30, - "line": 1, - "column": 20 - }, - "value": "B", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 29, - "end": 30 - } - } - }, - "rightExpression": { - "id": 10, - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 21 - }, - "fullStart": 31, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 33, - "start": 31, - "end": 33, - "expression": { - "id": 9, - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 21 - }, - "fullStart": 31, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 33, - "start": 31, - "end": 33, - "variable": { - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 21 - }, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 31, - "end": 33 - } - } - } - } - }, - "colon": { - "kind": "", - "startPos": { - "offset": 25, - "line": 1, - "column": 15 - }, - "endPos": { - "offset": 26, - "line": 1, - "column": 16 - }, - "value": ":", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 26, - "line": 1, - "column": 16 - }, - "endPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 26, - "end": 27 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 25, - "end": 26 - } - }, - "start": 22, - "end": 33, - "name": "CompileError" - }, { "code": 5001, "diagnostic": "References with same endpoints exist", diff --git a/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_2_elements.out.json b/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_2_elements.out.json index 8babb2c2f..9bad8d2da 100644 --- a/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_2_elements.out.json +++ b/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_2_elements.out.json @@ -1,1040 +1,220 @@ -[ - { - "code": 5001, - "diagnostic": "References with same endpoints exist", - "nodeOrToken": { - "id": 43, - "kind": "", - "startPos": { - "offset": 65, - "line": 9, - "column": 0 - }, - "fullStart": 65, - "endPos": { - "offset": 81, - "line": 9, - "column": 16 - }, - "fullEnd": 81, - "start": 65, - "end": 81, - "type": { - "kind": "", - "startPos": { - "offset": 65, - "line": 9, - "column": 0 - }, - "endPos": { - "offset": 68, - "line": 9, - "column": 3 +{ + "schemas": [], + "tables": [ + { + "name": "A", + "schemaName": null, + "alias": null, + "fields": [ + { + "name": "id", + "type": { + "schemaName": null, + "type_name": "int", + "args": null + }, + "token": { + "start": { + "offset": 14, + "line": 2, + "column": 5 + }, + "end": { + "offset": 20, + "line": 2, + "column": 11 + } + }, + "inline_refs": [], + "pk": false, + "unique": false + } + ], + "token": { + "start": { + "offset": 0, + "line": 1, + "column": 1 }, - "value": "Ref", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 65, - "end": 68 + "end": { + "offset": 22, + "line": 3, + "column": 2 + } }, - "bodyColon": { - "kind": "", - "startPos": { - "offset": 68, - "line": 9, - "column": 3 - }, - "endPos": { - "offset": 69, - "line": 9, - "column": 4 - }, - "value": ":", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 69, - "line": 9, - "column": 4 - }, - "endPos": { - "offset": 70, - "line": 9, + "indexes": [], + "partials": [], + "checks": [] + }, + { + "name": "B", + "schemaName": null, + "alias": null, + "fields": [ + { + "name": "id", + "type": { + "schemaName": null, + "type_name": "int", + "args": null + }, + "token": { + "start": { + "offset": 38, + "line": 6, "column": 5 }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 69, - "end": 70 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 68, - "end": 69 + "end": { + "offset": 44, + "line": 6, + "column": 11 + } + }, + "inline_refs": [], + "pk": false, + "unique": false + } + ], + "token": { + "start": { + "offset": 24, + "line": 5, + "column": 1 + }, + "end": { + "offset": 46, + "line": 7, + "column": 2 + } }, - "body": { - "id": 42, - "kind": "", - "startPos": { - "offset": 70, + "indexes": [], + "partials": [], + "checks": [] + } + ], + "notes": [], + "refs": [ + { + "token": { + "start": { + "offset": 48, "line": 9, - "column": 5 + "column": 1 }, - "fullStart": 70, - "endPos": { - "offset": 81, + "end": { + "offset": 64, "line": 9, - "column": 16 - }, - "fullEnd": 81, - "start": 70, - "end": 81, - "callee": { - "id": 41, - "kind": "", - "startPos": { - "offset": 70, - "line": 9, - "column": 5 - }, - "fullStart": 70, - "endPos": { - "offset": 81, - "line": 9, - "column": 16 - }, - "fullEnd": 81, - "start": 70, - "end": 81, - "op": { - "kind": "", - "startPos": { - "offset": 75, - "line": 9, - "column": 10 - }, - "endPos": { - "offset": 76, - "line": 9, - "column": 11 - }, - "value": ">", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 76, - "line": 9, - "column": 11 - }, - "endPos": { - "offset": 77, - "line": 9, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 76, - "end": 77 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 75, - "end": 76 - }, - "leftExpression": { - "id": 35, - "kind": "", - "startPos": { - "offset": 70, + "column": 17 + } + }, + "name": null, + "schemaName": null, + "endpoints": [ + { + "fieldNames": [ + "id" + ], + "tableName": "A", + "schemaName": null, + "relation": "*", + "token": { + "start": { + "offset": 53, "line": 9, - "column": 5 + "column": 6 }, - "fullStart": 70, - "endPos": { - "offset": 74, + "end": { + "offset": 57, "line": 9, - "column": 9 - }, - "fullEnd": 75, - "start": 70, - "end": 74, - "op": { - "kind": "", - "startPos": { - "offset": 71, - "line": 9, - "column": 6 - }, - "endPos": { - "offset": 72, - "line": 9, - "column": 7 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 71, - "end": 72 - }, - "leftExpression": { - "id": 32, - "kind": "", - "startPos": { - "offset": 70, - "line": 9, - "column": 5 - }, - "fullStart": 70, - "endPos": { - "offset": 71, - "line": 9, - "column": 6 - }, - "fullEnd": 71, - "start": 70, - "end": 71, - "expression": { - "id": 31, - "kind": "", - "startPos": { - "offset": 70, - "line": 9, - "column": 5 - }, - "fullStart": 70, - "endPos": { - "offset": 71, - "line": 9, - "column": 6 - }, - "fullEnd": 71, - "start": 70, - "end": 71, - "variable": { - "kind": "", - "startPos": { - "offset": 70, - "line": 9, - "column": 5 - }, - "endPos": { - "offset": 71, - "line": 9, - "column": 6 - }, - "value": "B", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 70, - "end": 71 - } - } - }, - "rightExpression": { - "id": 34, - "kind": "", - "startPos": { - "offset": 72, - "line": 9, - "column": 7 - }, - "fullStart": 72, - "endPos": { - "offset": 74, - "line": 9, - "column": 9 - }, - "fullEnd": 75, - "start": 72, - "end": 74, - "expression": { - "id": 33, - "kind": "", - "startPos": { - "offset": 72, - "line": 9, - "column": 7 - }, - "fullStart": 72, - "endPos": { - "offset": 74, - "line": 9, - "column": 9 - }, - "fullEnd": 75, - "start": 72, - "end": 74, - "variable": { - "kind": "", - "startPos": { - "offset": 72, - "line": 9, - "column": 7 - }, - "endPos": { - "offset": 74, - "line": 9, - "column": 9 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 74, - "line": 9, - "column": 9 - }, - "endPos": { - "offset": 75, - "line": 9, - "column": 10 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 74, - "end": 75 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 72, - "end": 74 - } - } + "column": 10 } - }, - "rightExpression": { - "id": 40, - "kind": "", - "startPos": { - "offset": 77, + } + }, + { + "fieldNames": [ + "id" + ], + "tableName": "B", + "schemaName": null, + "relation": "1", + "token": { + "start": { + "offset": 60, "line": 9, - "column": 12 + "column": 13 }, - "fullStart": 77, - "endPos": { - "offset": 81, + "end": { + "offset": 64, "line": 9, - "column": 16 - }, - "fullEnd": 81, - "start": 77, - "end": 81, - "op": { - "kind": "", - "startPos": { - "offset": 78, - "line": 9, - "column": 13 - }, - "endPos": { - "offset": 79, - "line": 9, - "column": 14 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 78, - "end": 79 - }, - "leftExpression": { - "id": 37, - "kind": "", - "startPos": { - "offset": 77, - "line": 9, - "column": 12 - }, - "fullStart": 77, - "endPos": { - "offset": 78, - "line": 9, - "column": 13 - }, - "fullEnd": 78, - "start": 77, - "end": 78, - "expression": { - "id": 36, - "kind": "", - "startPos": { - "offset": 77, - "line": 9, - "column": 12 - }, - "fullStart": 77, - "endPos": { - "offset": 78, - "line": 9, - "column": 13 - }, - "fullEnd": 78, - "start": 77, - "end": 78, - "variable": { - "kind": "", - "startPos": { - "offset": 77, - "line": 9, - "column": 12 - }, - "endPos": { - "offset": 78, - "line": 9, - "column": 13 - }, - "value": "A", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 77, - "end": 78 - } - } - }, - "rightExpression": { - "id": 39, - "kind": "", - "startPos": { - "offset": 79, - "line": 9, - "column": 14 - }, - "fullStart": 79, - "endPos": { - "offset": 81, - "line": 9, - "column": 16 - }, - "fullEnd": 81, - "start": 79, - "end": 81, - "expression": { - "id": 38, - "kind": "", - "startPos": { - "offset": 79, - "line": 9, - "column": 14 - }, - "fullStart": 79, - "endPos": { - "offset": 81, - "line": 9, - "column": 16 - }, - "fullEnd": 81, - "start": 79, - "end": 81, - "variable": { - "kind": "", - "startPos": { - "offset": 79, - "line": 9, - "column": 14 - }, - "endPos": { - "offset": 81, - "line": 9, - "column": 16 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 79, - "end": 81 - } - } + "column": 17 } } - }, - "args": [] - } + } + ] }, - "start": 65, - "end": 81, - "name": "CompileError" - }, - { - "code": 5001, - "diagnostic": "References with same endpoints exist", - "nodeOrToken": { - "id": 30, - "kind": "", - "startPos": { - "offset": 48, - "line": 8, - "column": 0 - }, - "fullStart": 47, - "endPos": { - "offset": 64, - "line": 8, - "column": 16 - }, - "fullEnd": 65, - "start": 48, - "end": 64, - "type": { - "kind": "", - "startPos": { - "offset": 48, - "line": 8, - "column": 0 - }, - "endPos": { - "offset": 51, - "line": 8, - "column": 3 + { + "token": { + "start": { + "offset": 65, + "line": 10, + "column": 1 }, - "value": "Ref", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 47, - "line": 7, - "column": 0 - }, - "endPos": { - "offset": 48, - "line": 8, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 47, - "end": 48 - } - ], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 48, - "end": 51 + "end": { + "offset": 81, + "line": 10, + "column": 17 + } }, - "bodyColon": { - "kind": "", - "startPos": { - "offset": 51, - "line": 8, - "column": 3 - }, - "endPos": { - "offset": 52, - "line": 8, - "column": 4 - }, - "value": ":", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 52, - "line": 8, - "column": 4 - }, - "endPos": { - "offset": 53, - "line": 8, - "column": 5 + "name": null, + "schemaName": null, + "endpoints": [ + { + "fieldNames": [ + "id" + ], + "tableName": "B", + "schemaName": null, + "relation": "*", + "token": { + "start": { + "offset": 70, + "line": 10, + "column": 6 }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 52, - "end": 53 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 51, - "end": 52 - }, - "body": { - "id": 29, - "kind": "", - "startPos": { - "offset": 53, - "line": 8, - "column": 5 - }, - "fullStart": 53, - "endPos": { - "offset": 64, - "line": 8, - "column": 16 - }, - "fullEnd": 65, - "start": 53, - "end": 64, - "callee": { - "id": 28, - "kind": "", - "startPos": { - "offset": 53, - "line": 8, - "column": 5 - }, - "fullStart": 53, - "endPos": { - "offset": 64, - "line": 8, - "column": 16 - }, - "fullEnd": 65, - "start": 53, - "end": 64, - "op": { - "kind": "", - "startPos": { - "offset": 58, - "line": 8, + "end": { + "offset": 74, + "line": 10, "column": 10 - }, - "endPos": { - "offset": 59, - "line": 8, - "column": 11 - }, - "value": ">", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 59, - "line": 8, - "column": 11 - }, - "endPos": { - "offset": 60, - "line": 8, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 59, - "end": 60 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 58, - "end": 59 - }, - "leftExpression": { - "id": 22, - "kind": "", - "startPos": { - "offset": 53, - "line": 8, - "column": 5 - }, - "fullStart": 53, - "endPos": { - "offset": 57, - "line": 8, - "column": 9 - }, - "fullEnd": 58, - "start": 53, - "end": 57, - "op": { - "kind": "", - "startPos": { - "offset": 54, - "line": 8, - "column": 6 - }, - "endPos": { - "offset": 55, - "line": 8, - "column": 7 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 54, - "end": 55 - }, - "leftExpression": { - "id": 19, - "kind": "", - "startPos": { - "offset": 53, - "line": 8, - "column": 5 - }, - "fullStart": 53, - "endPos": { - "offset": 54, - "line": 8, - "column": 6 - }, - "fullEnd": 54, - "start": 53, - "end": 54, - "expression": { - "id": 18, - "kind": "", - "startPos": { - "offset": 53, - "line": 8, - "column": 5 - }, - "fullStart": 53, - "endPos": { - "offset": 54, - "line": 8, - "column": 6 - }, - "fullEnd": 54, - "start": 53, - "end": 54, - "variable": { - "kind": "", - "startPos": { - "offset": 53, - "line": 8, - "column": 5 - }, - "endPos": { - "offset": 54, - "line": 8, - "column": 6 - }, - "value": "A", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 53, - "end": 54 - } - } - }, - "rightExpression": { - "id": 21, - "kind": "", - "startPos": { - "offset": 55, - "line": 8, - "column": 7 - }, - "fullStart": 55, - "endPos": { - "offset": 57, - "line": 8, - "column": 9 - }, - "fullEnd": 58, - "start": 55, - "end": 57, - "expression": { - "id": 20, - "kind": "", - "startPos": { - "offset": 55, - "line": 8, - "column": 7 - }, - "fullStart": 55, - "endPos": { - "offset": 57, - "line": 8, - "column": 9 - }, - "fullEnd": 58, - "start": 55, - "end": 57, - "variable": { - "kind": "", - "startPos": { - "offset": 55, - "line": 8, - "column": 7 - }, - "endPos": { - "offset": 57, - "line": 8, - "column": 9 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 57, - "line": 8, - "column": 9 - }, - "endPos": { - "offset": 58, - "line": 8, - "column": 10 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 57, - "end": 58 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 55, - "end": 57 - } - } } - }, - "rightExpression": { - "id": 27, - "kind": "", - "startPos": { - "offset": 60, - "line": 8, - "column": 12 - }, - "fullStart": 60, - "endPos": { - "offset": 64, - "line": 8, - "column": 16 - }, - "fullEnd": 65, - "start": 60, - "end": 64, - "op": { - "kind": "", - "startPos": { - "offset": 61, - "line": 8, - "column": 13 - }, - "endPos": { - "offset": 62, - "line": 8, - "column": 14 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 61, - "end": 62 - }, - "leftExpression": { - "id": 24, - "kind": "", - "startPos": { - "offset": 60, - "line": 8, - "column": 12 - }, - "fullStart": 60, - "endPos": { - "offset": 61, - "line": 8, - "column": 13 - }, - "fullEnd": 61, - "start": 60, - "end": 61, - "expression": { - "id": 23, - "kind": "", - "startPos": { - "offset": 60, - "line": 8, - "column": 12 - }, - "fullStart": 60, - "endPos": { - "offset": 61, - "line": 8, - "column": 13 - }, - "fullEnd": 61, - "start": 60, - "end": 61, - "variable": { - "kind": "", - "startPos": { - "offset": 60, - "line": 8, - "column": 12 - }, - "endPos": { - "offset": 61, - "line": 8, - "column": 13 - }, - "value": "B", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 60, - "end": 61 - } - } + } + }, + { + "fieldNames": [ + "id" + ], + "tableName": "A", + "schemaName": null, + "relation": "1", + "token": { + "start": { + "offset": 77, + "line": 10, + "column": 13 }, - "rightExpression": { - "id": 26, - "kind": "", - "startPos": { - "offset": 62, - "line": 8, - "column": 14 - }, - "fullStart": 62, - "endPos": { - "offset": 64, - "line": 8, - "column": 16 - }, - "fullEnd": 65, - "start": 62, - "end": 64, - "expression": { - "id": 25, - "kind": "", - "startPos": { - "offset": 62, - "line": 8, - "column": 14 - }, - "fullStart": 62, - "endPos": { - "offset": 64, - "line": 8, - "column": 16 - }, - "fullEnd": 65, - "start": 62, - "end": 64, - "variable": { - "kind": "", - "startPos": { - "offset": 62, - "line": 8, - "column": 14 - }, - "endPos": { - "offset": 64, - "line": 8, - "column": 16 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 64, - "line": 8, - "column": 16 - }, - "endPos": { - "offset": 65, - "line": 9, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 64, - "end": 65 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 62, - "end": 64 - } - } + "end": { + "offset": 81, + "line": 10, + "column": 17 } } - }, - "args": [] - } - }, - "start": 48, - "end": 64, - "name": "CompileError" - } -] \ No newline at end of file + } + ] + } + ], + "enums": [], + "tableGroups": [], + "aliases": [], + "project": {}, + "tablePartials": [], + "records": [] +} \ No newline at end of file diff --git a/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_2_inlines.out.json b/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_2_inlines.out.json index 6e71e4346..d22f122f0 100644 --- a/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_2_inlines.out.json +++ b/packages/dbml-parse/__tests__/snapshots/interpreter/output/circular_ref_2_inlines.out.json @@ -1,648 +1,266 @@ -[ - { - "code": 5001, - "diagnostic": "References with same endpoints exist", - "nodeOrToken": { - "id": 31, - "kind": "", - "startPos": { - "offset": 60, - "line": 5, - "column": 12 - }, - "fullStart": 60, - "endPos": { - "offset": 71, - "line": 5, - "column": 23 - }, - "fullEnd": 71, - "start": 60, - "end": 71, - "name": { - "id": 24, - "kind": "", - "startPos": { - "offset": 60, - "line": 5, - "column": 12 - }, - "fullStart": 60, - "endPos": { - "offset": 63, - "line": 5, - "column": 15 - }, - "fullEnd": 63, - "start": 60, - "end": 63, - "identifiers": [ - { - "kind": "", - "startPos": { - "offset": 60, - "line": 5, - "column": 12 - }, - "endPos": { - "offset": 63, - "line": 5, - "column": 15 - }, - "value": "ref", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 60, - "end": 63 - } - ] - }, - "value": { - "id": 30, - "kind": "", - "startPos": { - "offset": 65, - "line": 5, - "column": 17 - }, - "fullStart": 65, - "endPos": { - "offset": 71, - "line": 5, - "column": 23 - }, - "fullEnd": 71, - "start": 65, - "end": 71, - "op": { - "kind": "", - "startPos": { - "offset": 65, - "line": 5, - "column": 17 +{ + "schemas": [], + "tables": [ + { + "name": "A", + "schemaName": null, + "alias": null, + "fields": [ + { + "name": "id", + "type": { + "schemaName": null, + "type_name": "int", + "args": null }, - "endPos": { - "offset": 66, - "line": 5, - "column": 18 + "token": { + "start": { + "offset": 14, + "line": 2, + "column": 5 + }, + "end": { + "offset": 34, + "line": 2, + "column": 25 + } }, - "value": ">", - "leadingTrivia": [], - "trailingTrivia": [ + "inline_refs": [ { - "kind": "", - "startPos": { - "offset": 66, - "line": 5, - "column": 18 - }, - "endPos": { - "offset": 67, - "line": 5, - "column": 19 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 66, - "end": 67 + "schemaName": null, + "tableName": "B", + "fieldNames": [ + "id" + ], + "relation": ">", + "token": { + "start": { + "offset": 22, + "line": 2, + "column": 13 + }, + "end": { + "offset": 33, + "line": 2, + "column": 24 + } + } } ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 65, - "end": 66 + "pk": false, + "increment": false, + "unique": false, + "checks": [] + } + ], + "token": { + "start": { + "offset": 0, + "line": 1, + "column": 1 }, - "expression": { - "id": 29, - "kind": "", - "startPos": { - "offset": 67, - "line": 5, - "column": 19 - }, - "fullStart": 67, - "endPos": { - "offset": 71, - "line": 5, - "column": 23 - }, - "fullEnd": 71, - "start": 67, - "end": 71, - "op": { - "kind": "", - "startPos": { - "offset": 68, - "line": 5, - "column": 20 - }, - "endPos": { - "offset": 69, - "line": 5, - "column": 21 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 68, - "end": 69 + "end": { + "offset": 36, + "line": 3, + "column": 2 + } + }, + "indexes": [], + "partials": [], + "checks": [] + }, + { + "name": "B", + "schemaName": null, + "alias": null, + "fields": [ + { + "name": "id", + "type": { + "schemaName": null, + "type_name": "int", + "args": null }, - "leftExpression": { - "id": 26, - "kind": "", - "startPos": { - "offset": 67, - "line": 5, - "column": 19 + "token": { + "start": { + "offset": 52, + "line": 6, + "column": 5 }, - "fullStart": 67, - "endPos": { - "offset": 68, - "line": 5, - "column": 20 - }, - "fullEnd": 68, - "start": 67, - "end": 68, - "expression": { - "id": 25, - "kind": "", - "startPos": { - "offset": 67, - "line": 5, - "column": 19 - }, - "fullStart": 67, - "endPos": { - "offset": 68, - "line": 5, - "column": 20 - }, - "fullEnd": 68, - "start": 67, - "end": 68, - "variable": { - "kind": "", - "startPos": { - "offset": 67, - "line": 5, - "column": 19 - }, - "endPos": { - "offset": 68, - "line": 5, - "column": 20 - }, - "value": "A", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 67, - "end": 68 - } + "end": { + "offset": 72, + "line": 6, + "column": 25 } }, - "rightExpression": { - "id": 28, - "kind": "", - "startPos": { - "offset": 69, - "line": 5, - "column": 21 - }, - "fullStart": 69, - "endPos": { - "offset": 71, - "line": 5, - "column": 23 - }, - "fullEnd": 71, - "start": 69, - "end": 71, - "expression": { - "id": 27, - "kind": "", - "startPos": { - "offset": 69, - "line": 5, - "column": 21 - }, - "fullStart": 69, - "endPos": { - "offset": 71, - "line": 5, - "column": 23 - }, - "fullEnd": 71, - "start": 69, - "end": 71, - "variable": { - "kind": "", - "startPos": { - "offset": 69, - "line": 5, - "column": 21 + "inline_refs": [ + { + "schemaName": null, + "tableName": "A", + "fieldNames": [ + "id" + ], + "relation": ">", + "token": { + "start": { + "offset": 60, + "line": 6, + "column": 13 }, - "endPos": { + "end": { "offset": 71, - "line": 5, - "column": 23 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 69, - "end": 71 + "line": 6, + "column": 24 + } } } - } + ], + "pk": false, + "increment": false, + "unique": false, + "checks": [] } - }, - "colon": { - "kind": "", - "startPos": { - "offset": 63, - "line": 5, - "column": 15 - }, - "endPos": { - "offset": 64, + ], + "token": { + "start": { + "offset": 38, "line": 5, - "column": 16 + "column": 1 }, - "value": ":", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 64, - "line": 5, - "column": 16 - }, - "endPos": { - "offset": 65, - "line": 5, - "column": 17 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 64, - "end": 65 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 63, - "end": 64 - } - }, - "start": 60, - "end": 71, - "name": "CompileError" - }, - { - "code": 5001, - "diagnostic": "References with same endpoints exist", - "nodeOrToken": { - "id": 13, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "fullStart": 22, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 + "end": { + "offset": 74, + "line": 7, + "column": 2 + } }, - "fullEnd": 33, - "start": 22, - "end": 33, - "name": { - "id": 6, - "kind": "", - "startPos": { + "indexes": [], + "partials": [], + "checks": [] + } + ], + "notes": [], + "refs": [ + { + "name": null, + "schemaName": null, + "token": { + "start": { "offset": 22, - "line": 1, - "column": 12 - }, - "fullStart": 22, - "endPos": { - "offset": 25, - "line": 1, - "column": 15 + "line": 2, + "column": 13 }, - "fullEnd": 25, - "start": 22, - "end": 25, - "identifiers": [ - { - "kind": "", - "startPos": { + "end": { + "offset": 33, + "line": 2, + "column": 24 + } + }, + "endpoints": [ + { + "schemaName": null, + "tableName": "B", + "fieldNames": [ + "id" + ], + "relation": "1", + "token": { + "start": { "offset": 22, - "line": 1, - "column": 12 + "line": 2, + "column": 13 }, - "endPos": { - "offset": 25, - "line": 1, - "column": 15 - }, - "value": "ref", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 22, - "end": 25 + "end": { + "offset": 33, + "line": 2, + "column": 24 + } } - ] - }, - "value": { - "id": 12, - "kind": "", - "startPos": { - "offset": 27, - "line": 1, - "column": 17 }, - "fullStart": 27, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 33, - "start": 27, - "end": 33, - "op": { - "kind": "", - "startPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "endPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "value": ">", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "endPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 28, - "end": 29 - } + { + "schemaName": null, + "tableName": "A", + "fieldNames": [ + "id" ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 27, - "end": 28 - }, - "expression": { - "id": 11, - "kind": "", - "startPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "fullStart": 29, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 33, - "start": 29, - "end": 33, - "op": { - "kind": "", - "startPos": { - "offset": 30, - "line": 1, - "column": 20 - }, - "endPos": { - "offset": 31, - "line": 1, - "column": 21 + "token": { + "start": { + "offset": 14, + "line": 2, + "column": 5 }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 30, - "end": 31 - }, - "leftExpression": { - "id": 8, - "kind": "", - "startPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "fullStart": 29, - "endPos": { - "offset": 30, - "line": 1, - "column": 20 - }, - "fullEnd": 30, - "start": 29, - "end": 30, - "expression": { - "id": 7, - "kind": "", - "startPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "fullStart": 29, - "endPos": { - "offset": 30, - "line": 1, - "column": 20 - }, - "fullEnd": 30, - "start": 29, - "end": 30, - "variable": { - "kind": "", - "startPos": { - "offset": 29, - "line": 1, - "column": 19 - }, - "endPos": { - "offset": 30, - "line": 1, - "column": 20 - }, - "value": "B", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 29, - "end": 30 - } + "end": { + "offset": 34, + "line": 2, + "column": 25 } }, - "rightExpression": { - "id": 10, - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 21 - }, - "fullStart": 31, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 + "relation": "*" + } + ] + }, + { + "name": null, + "schemaName": null, + "token": { + "start": { + "offset": 60, + "line": 6, + "column": 13 + }, + "end": { + "offset": 71, + "line": 6, + "column": 24 + } + }, + "endpoints": [ + { + "schemaName": null, + "tableName": "A", + "fieldNames": [ + "id" + ], + "relation": "1", + "token": { + "start": { + "offset": 60, + "line": 6, + "column": 13 }, - "fullEnd": 33, - "start": 31, - "end": 33, - "expression": { - "id": 9, - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 21 - }, - "fullStart": 31, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 33, - "start": 31, - "end": 33, - "variable": { - "kind": "", - "startPos": { - "offset": 31, - "line": 1, - "column": 21 - }, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "value": "id", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 31, - "end": 33 - } + "end": { + "offset": 71, + "line": 6, + "column": 24 } } - } - }, - "colon": { - "kind": "", - "startPos": { - "offset": 25, - "line": 1, - "column": 15 }, - "endPos": { - "offset": 26, - "line": 1, - "column": 16 - }, - "value": ":", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 26, - "line": 1, - "column": 16 - }, - "endPos": { - "offset": 27, - "line": 1, - "column": 17 + { + "schemaName": null, + "tableName": "B", + "fieldNames": [ + "id" + ], + "token": { + "start": { + "offset": 52, + "line": 6, + "column": 5 }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 26, - "end": 27 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 25, - "end": 26 - } - }, - "start": 22, - "end": 33, - "name": "CompileError" - } -] \ No newline at end of file + "end": { + "offset": 72, + "line": 6, + "column": 25 + } + }, + "relation": "*" + } + ] + } + ], + "enums": [], + "tableGroups": [], + "aliases": [], + "project": {}, + "tablePartials": [], + "records": [] +} \ No newline at end of file diff --git a/packages/dbml-parse/__tests__/snapshots/interpreter/output/erroneous.out.json b/packages/dbml-parse/__tests__/snapshots/interpreter/output/erroneous.out.json index 69fc16a2c..4434afd5f 100644 --- a/packages/dbml-parse/__tests__/snapshots/interpreter/output/erroneous.out.json +++ b/packages/dbml-parse/__tests__/snapshots/interpreter/output/erroneous.out.json @@ -198,157 +198,5 @@ "start": 226, "end": 227, "name": "CompileError" - }, - { - "code": 3021, - "diagnostic": "Unknown column setting 'diagram_id'", - "nodeOrToken": { - "id": 37, - "kind": "", - "startPos": { - "offset": 205, - "line": 9, - "column": 14 - }, - "fullStart": 205, - "endPos": { - "offset": 215, - "line": 9, - "column": 24 - }, - "fullEnd": 215, - "start": 205, - "end": 215, - "name": { - "id": 36, - "kind": "", - "startPos": { - "offset": 205, - "line": 9, - "column": 14 - }, - "fullStart": 205, - "endPos": { - "offset": 215, - "line": 9, - "column": 24 - }, - "fullEnd": 215, - "start": 205, - "end": 215, - "identifiers": [ - { - "kind": "", - "startPos": { - "offset": 205, - "line": 9, - "column": 14 - }, - "endPos": { - "offset": 215, - "line": 9, - "column": 24 - }, - "value": "diagram_id", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [ - { - "kind": "", - "startPos": { - "offset": 215, - "line": 9, - "column": 24 - }, - "endPos": { - "offset": 216, - "line": 9, - "column": 25 - }, - "value": ")", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 216, - "line": 9, - "column": 25 - }, - "endPos": { - "offset": 217, - "line": 9, - "column": 26 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 216, - "end": 217 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": true, - "start": 215, - "end": 216 - }, - { - "kind": "", - "startPos": { - "offset": 217, - "line": 9, - "column": 26 - }, - "endPos": { - "offset": 218, - "line": 9, - "column": 27 - }, - "value": "[", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": true, - "start": 217, - "end": 218 - }, - { - "kind": "", - "startPos": { - "offset": 218, - "line": 9, - "column": 27 - }, - "endPos": { - "offset": 220, - "line": 9, - "column": 29 - }, - "value": "pk", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": true, - "start": 218, - "end": 220 - } - ], - "isInvalid": false, - "start": 205, - "end": 215 - } - ] - } - }, - "start": 205, - "end": 215, - "name": "CompileError" } ] \ No newline at end of file diff --git a/packages/dbml-parse/__tests__/snapshots/interpreter/output/table_element.out.json b/packages/dbml-parse/__tests__/snapshots/interpreter/output/table_element.out.json index 7a64b9b1b..0b588a642 100644 --- a/packages/dbml-parse/__tests__/snapshots/interpreter/output/table_element.out.json +++ b/packages/dbml-parse/__tests__/snapshots/interpreter/output/table_element.out.json @@ -1,717 +1,291 @@ -[ - { - "code": 3044, - "diagnostic": "Duplicate notes are defined", - "nodeOrToken": { - "id": 36, - "kind": "", - "startPos": { - "offset": 123, - "line": 6, - "column": 2 - }, - "fullStart": 120, - "endPos": { - "offset": 141, - "line": 6, - "column": 20 - }, - "fullEnd": 142, - "start": 123, - "end": 141, - "type": { - "kind": "", - "startPos": { - "offset": 123, - "line": 6, - "column": 2 - }, - "endPos": { - "offset": 127, - "line": 6, - "column": 6 - }, - "value": "Note", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 120, - "line": 5, - "column": 0 - }, - "endPos": { - "offset": 121, - "line": 6, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 120, - "end": 121 +{ + "schemas": [], + "tables": [ + { + "name": "users", + "schemaName": null, + "alias": null, + "fields": [ + { + "name": "id", + "type": { + "schemaName": null, + "type_name": "int", + "args": null }, - { - "kind": "", - "startPos": { - "offset": 121, - "line": 6, - "column": 0 - }, - "endPos": { - "offset": 122, - "line": 6, - "column": 1 + "token": { + "start": { + "offset": 49, + "line": 2, + "column": 3 }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 121, - "end": 122 + "end": { + "offset": 60, + "line": 2, + "column": 14 + } }, - { - "kind": "", - "startPos": { - "offset": 122, - "line": 6, - "column": 1 - }, - "endPos": { - "offset": 123, - "line": 6, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 122, - "end": 123 - } - ], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 123, - "end": 127 - }, - "bodyColon": { - "kind": "", - "startPos": { - "offset": 127, - "line": 6, - "column": 6 - }, - "endPos": { - "offset": 128, - "line": 6, - "column": 7 - }, - "value": ":", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 128, - "line": 6, - "column": 7 - }, - "endPos": { - "offset": 129, - "line": 6, - "column": 8 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 128, - "end": 129 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 127, - "end": 128 - }, - "body": { - "id": 35, - "kind": "", - "startPos": { - "offset": 129, - "line": 6, - "column": 8 + "inline_refs": [], + "pk": true, + "increment": false, + "unique": false, + "checks": [] }, - "fullStart": 129, - "endPos": { - "offset": 141, - "line": 6, - "column": 20 - }, - "fullEnd": 142, - "start": 129, - "end": 141, - "callee": { - "id": 34, - "kind": "", - "startPos": { - "offset": 129, - "line": 6, - "column": 8 - }, - "fullStart": 129, - "endPos": { - "offset": 141, - "line": 6, - "column": 20 + { + "name": "name", + "type": { + "schemaName": null, + "type_name": "varchar", + "args": null }, - "fullEnd": 142, - "start": 129, - "end": 141, - "expression": { - "id": 33, - "kind": "", - "startPos": { - "offset": 129, - "line": 6, - "column": 8 + "token": { + "start": { + "offset": 63, + "line": 3, + "column": 3 }, - "fullStart": 129, - "endPos": { - "offset": 141, - "line": 6, + "end": { + "offset": 80, + "line": 3, "column": 20 - }, - "fullEnd": 142, - "start": 129, - "end": 141, - "literal": { - "kind": "", - "startPos": { - "offset": 129, - "line": 6, - "column": 8 - }, - "endPos": { - "offset": 141, - "line": 6, - "column": 20 - }, - "value": "Short note", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 141, - "line": 6, - "column": 20 - }, - "endPos": { - "offset": 142, - "line": 7, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 141, - "end": 142 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 129, - "end": 141 } - } - }, - "args": [] - } - }, - "start": 123, - "end": 141, - "name": "CompileError" - }, - { - "code": 3044, - "diagnostic": "Duplicate notes are defined", - "nodeOrToken": { - "id": 68, - "kind": "", - "startPos": { - "offset": 227, - "line": 18, - "column": 2 - }, - "fullStart": 224, - "endPos": { - "offset": 405, - "line": 27, - "column": 3 - }, - "fullEnd": 406, - "start": 227, - "end": 405, - "type": { - "kind": "", - "startPos": { - "offset": 227, - "line": 18, - "column": 2 - }, - "endPos": { - "offset": 231, - "line": 18, - "column": 6 + }, + "inline_refs": [], + "pk": true, + "increment": false, + "unique": false, + "checks": [] }, - "value": "Note", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 224, - "line": 17, - "column": 0 - }, - "endPos": { - "offset": 225, - "line": 18, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 224, - "end": 225 + { + "name": "gender", + "type": { + "schemaName": null, + "type_name": "varchar", + "args": null }, - { - "kind": "", - "startPos": { - "offset": 225, - "line": 18, - "column": 0 - }, - "endPos": { - "offset": 226, - "line": 18, - "column": 1 + "token": { + "start": { + "offset": 83, + "line": 4, + "column": 3 }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 225, - "end": 226 + "end": { + "offset": 97, + "line": 4, + "column": 17 + } }, - { - "kind": "", - "startPos": { - "offset": 226, - "line": 18, - "column": 1 - }, - "endPos": { - "offset": 227, - "line": 18, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 226, - "end": 227 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 231, - "line": 18, - "column": 6 - }, - "endPos": { - "offset": 232, - "line": 18, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 231, - "end": 232 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 227, - "end": 231 - }, - "body": { - "id": 67, - "kind": "", - "startPos": { - "offset": 232, - "line": 18, - "column": 7 + "inline_refs": [], + "pk": false, + "unique": false }, - "fullStart": 232, - "endPos": { - "offset": 405, - "line": 27, - "column": 3 - }, - "fullEnd": 406, - "start": 232, - "end": 405, - "blockOpenBrace": { - "kind": "", - "startPos": { - "offset": 232, - "line": 18, - "column": 7 + { + "name": "created_at", + "type": { + "schemaName": null, + "type_name": "datetime", + "args": null }, - "endPos": { - "offset": 233, - "line": 18, - "column": 8 + "token": { + "start": { + "offset": 100, + "line": 5, + "column": 3 + }, + "end": { + "offset": 119, + "line": 5, + "column": 22 + } }, - "value": "{", - "leadingTrivia": [], - "trailingTrivia": [ + "inline_refs": [], + "pk": false, + "unique": false + } + ], + "token": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 407, + "line": 29, + "column": 2 + } + }, + "indexes": [ + { + "columns": [ { - "kind": "", - "startPos": { - "offset": 233, - "line": 18, - "column": 8 - }, - "endPos": { - "offset": 234, - "line": 19, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 233, - "end": 234 + "value": "id", + "type": "column", + "token": { + "start": { + "offset": 159, + "line": 10, + "column": 5 + }, + "end": { + "offset": 161, + "line": 10, + "column": 7 + } + } } ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 232, - "end": 233 - }, - "body": [ - { - "id": 66, - "kind": "", - "startPos": { - "offset": 238, - "line": 19, - "column": 4 + "token": { + "start": { + "offset": 159, + "line": 10, + "column": 5 }, - "fullStart": 234, - "endPos": { - "offset": 401, - "line": 26, + "end": { + "offset": 161, + "line": 10, "column": 7 - }, - "fullEnd": 402, - "start": 238, - "end": 401, - "callee": { - "id": 65, - "kind": "", - "startPos": { - "offset": 238, - "line": 19, - "column": 4 - }, - "fullStart": 234, - "endPos": { - "offset": 401, - "line": 26, - "column": 7 - }, - "fullEnd": 402, - "start": 238, - "end": 401, - "expression": { - "id": 64, - "kind": "", - "startPos": { - "offset": 238, - "line": 19, - "column": 4 - }, - "fullStart": 234, - "endPos": { - "offset": 401, - "line": 26, - "column": 7 + } + } + }, + { + "columns": [ + { + "value": "id", + "type": "column", + "token": { + "start": { + "offset": 167, + "line": 11, + "column": 6 }, - "fullEnd": 402, - "start": 238, - "end": 401, - "literal": { - "kind": "", - "startPos": { - "offset": 238, - "line": 19, - "column": 4 - }, - "endPos": { - "offset": 401, - "line": 26, - "column": 7 - }, - "value": "\n # Note\n\n ## Objective\n * Support define element's note inside element body\n * Make writing long note easier with the new syntax\n \n ", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 234, - "line": 19, - "column": 0 - }, - "endPos": { - "offset": 235, - "line": 19, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 234, - "end": 235 - }, - { - "kind": "", - "startPos": { - "offset": 235, - "line": 19, - "column": 1 - }, - "endPos": { - "offset": 236, - "line": 19, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 235, - "end": 236 - }, - { - "kind": "", - "startPos": { - "offset": 236, - "line": 19, - "column": 2 - }, - "endPos": { - "offset": 237, - "line": 19, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 236, - "end": 237 - }, - { - "kind": "", - "startPos": { - "offset": 237, - "line": 19, - "column": 3 - }, - "endPos": { - "offset": 238, - "line": 19, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 237, - "end": 238 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 401, - "line": 26, - "column": 7 - }, - "endPos": { - "offset": 402, - "line": 27, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 401, - "end": 402 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 238, - "end": 401 + "end": { + "offset": 169, + "line": 11, + "column": 8 } } }, - "args": [] - } - ], - "blockCloseBrace": { - "kind": "", - "startPos": { - "offset": 404, - "line": 27, - "column": 2 - }, - "endPos": { - "offset": 405, - "line": 27, - "column": 3 - }, - "value": "}", - "leadingTrivia": [ { - "kind": "", - "startPos": { - "offset": 402, - "line": 27, - "column": 0 - }, - "endPos": { - "offset": 403, - "line": 27, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 402, - "end": 403 + "value": "name", + "type": "column", + "token": { + "start": { + "offset": 171, + "line": 11, + "column": 10 + }, + "end": { + "offset": 175, + "line": 11, + "column": 14 + } + } + } + ], + "token": { + "start": { + "offset": 166, + "line": 11, + "column": 5 }, + "end": { + "offset": 176, + "line": 11, + "column": 15 + } + } + }, + { + "columns": [ { - "kind": "", - "startPos": { - "offset": 403, - "line": 27, - "column": 1 - }, - "endPos": { - "offset": 404, - "line": 27, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 403, - "end": 404 + "value": "gender", + "type": "column", + "token": { + "start": { + "offset": 198, + "line": 15, + "column": 5 + }, + "end": { + "offset": 204, + "line": 15, + "column": 11 + } + } } ], - "trailingTrivia": [ + "token": { + "start": { + "offset": 198, + "line": 15, + "column": 5 + }, + "end": { + "offset": 204, + "line": 15, + "column": 11 + } + } + }, + { + "columns": [ { - "kind": "", - "startPos": { - "offset": 405, - "line": 27, - "column": 3 - }, - "endPos": { - "offset": 406, - "line": 28, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 405, - "end": 406 + "value": "created_at", + "type": "column", + "token": { + "start": { + "offset": 209, + "line": 16, + "column": 5 + }, + "end": { + "offset": 219, + "line": 16, + "column": 15 + } + } } ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 404, - "end": 405 + "token": { + "start": { + "offset": 209, + "line": 16, + "column": 5 + }, + "end": { + "offset": 219, + "line": 16, + "column": 15 + } + } + } + ], + "partials": [], + "checks": [], + "note": { + "value": "note in table settings", + "token": { + "start": { + "offset": 13, + "line": 1, + "column": 14 + }, + "end": { + "offset": 43, + "line": 1, + "column": 44 + } } } - }, - "start": 227, - "end": 405, - "name": "CompileError" - } -] \ No newline at end of file + } + ], + "notes": [], + "refs": [], + "enums": [], + "tableGroups": [], + "aliases": [], + "project": {}, + "tablePartials": [], + "records": [] +} \ No newline at end of file diff --git a/packages/dbml-parse/__tests__/snapshots/interpreter/output/table_reappear_tablegroup.out.json b/packages/dbml-parse/__tests__/snapshots/interpreter/output/table_reappear_tablegroup.out.json index 6b7cb9284..156e75371 100644 --- a/packages/dbml-parse/__tests__/snapshots/interpreter/output/table_reappear_tablegroup.out.json +++ b/packages/dbml-parse/__tests__/snapshots/interpreter/output/table_reappear_tablegroup.out.json @@ -1,4 +1,133 @@ [ + { + "code": 4000, + "diagnostic": "Table 'U2' does not exist in Schema 'public'", + "nodeOrToken": { + "id": 96, + "kind": "", + "startPos": { + "offset": 378, + "line": 35, + "column": 2 + }, + "fullStart": 376, + "endPos": { + "offset": 380, + "line": 35, + "column": 4 + }, + "fullEnd": 381, + "start": 378, + "end": 380, + "expression": { + "id": 95, + "kind": "", + "startPos": { + "offset": 378, + "line": 35, + "column": 2 + }, + "fullStart": 376, + "endPos": { + "offset": 380, + "line": 35, + "column": 4 + }, + "fullEnd": 381, + "start": 378, + "end": 380, + "variable": { + "kind": "", + "startPos": { + "offset": 378, + "line": 35, + "column": 2 + }, + "endPos": { + "offset": 380, + "line": 35, + "column": 4 + }, + "value": "U2", + "leadingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 376, + "line": 35, + "column": 0 + }, + "endPos": { + "offset": 377, + "line": 35, + "column": 1 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 376, + "end": 377 + }, + { + "kind": "", + "startPos": { + "offset": 377, + "line": 35, + "column": 1 + }, + "endPos": { + "offset": 378, + "line": 35, + "column": 2 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 377, + "end": 378 + } + ], + "trailingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 380, + "line": 35, + "column": 4 + }, + "endPos": { + "offset": 381, + "line": 36, + "column": 0 + }, + "value": "\n", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 380, + "end": 381 + } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 378, + "end": 380 + } + } + }, + "start": 378, + "end": 380, + "name": "CompileError" + }, { "code": 5005, "diagnostic": "Table \"follows\" already appears in group \"A1\"", diff --git a/packages/dbml-parse/__tests__/snapshots/lexer/lexer.test.ts b/packages/dbml-parse/__tests__/snapshots/lexer/lexer.test.ts index 06083a9e7..900e14969 100644 --- a/packages/dbml-parse/__tests__/snapshots/lexer/lexer.test.ts +++ b/packages/dbml-parse/__tests__/snapshots/lexer/lexer.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'fs'; import path from 'path'; import { describe, expect, it } from 'vitest'; -import Lexer from '@/core/lexer/lexer'; +import Lexer from '@/core/syntax/lexer/lexer'; import { scanTestNames } from '@tests/utils'; describe('[snapshot] lexer', () => { diff --git a/packages/dbml-parse/__tests__/snapshots/nan/nan.test.ts b/packages/dbml-parse/__tests__/snapshots/nan/nan.test.ts index 7b3ba3240..eadae6cc3 100644 --- a/packages/dbml-parse/__tests__/snapshots/nan/nan.test.ts +++ b/packages/dbml-parse/__tests__/snapshots/nan/nan.test.ts @@ -1,40 +1,34 @@ import { readFileSync } from 'fs'; import path from 'path'; import { describe, expect, it } from 'vitest'; -import { scanTestNames } from '../../utils'; -import { NodeSymbolIdGenerator } from '@/core/analyzer/symbol/symbols'; -import { SyntaxNodeIdGenerator } from '@/core/parser/nodes'; -import Lexer from '@/core/lexer/lexer'; -import Parser from '@/core/parser/parser'; -import Analyzer from '@/core/analyzer/analyzer'; -import Interpreter from '@/core/interpreter/interpreter'; +import { Compiler } from '@/index'; +import { UNHANDLED } from '@/constants'; +import { scanTestNames } from '../../../utils'; describe('[snapshot] interpreter (NaN cases)', () => { const testNames = scanTestNames(path.resolve(__dirname, './input/')); testNames.forEach((testName) => { const program = readFileSync(path.resolve(__dirname, `./input/${testName}.in.dbml`), 'utf-8'); - const symbolIdGenerator = new NodeSymbolIdGenerator(); - const nodeIdGenerator = new SyntaxNodeIdGenerator(); + const compiler = new Compiler(); + compiler.setSource(program); let output: any; - const report = new Lexer(program) - .lex() - .chain((tokens) => { - return new Parser(program, tokens, nodeIdGenerator).parse(); - }) - .chain(({ ast }) => { - return new Analyzer(ast, symbolIdGenerator).analyze(); - }); - if (report.getErrors().length !== 0) { + const ast = compiler.parseFile().getValue().ast; + const bindResult = compiler.bind(ast); + const bindErrors = [...compiler.parseFile().getErrors(), ...bindResult.getErrors()]; + + if (bindErrors.length !== 0) { output = JSON.stringify( - report.getErrors(), + bindErrors, (key, value) => (['symbol', 'references', 'referee', 'parent'].includes(key) ? undefined : value), 2, ); } else { - const res = new Interpreter(report.getValue()).interpret(); - if (res.getErrors().length > 0) { + const res = compiler.interpret(ast); + if (res.hasValue(UNHANDLED)) { + output = JSON.stringify(undefined, null, 2); + } else if (res.getErrors().length > 0) { output = JSON.stringify( res.getErrors(), (key, value) => (['symbol', 'references', 'referee', 'parent'].includes(key) ? undefined : value), diff --git a/packages/dbml-parse/__tests__/snapshots/parser/parser.test.ts b/packages/dbml-parse/__tests__/snapshots/parser/parser.test.ts index ad81622d7..f1cd1714f 100644 --- a/packages/dbml-parse/__tests__/snapshots/parser/parser.test.ts +++ b/packages/dbml-parse/__tests__/snapshots/parser/parser.test.ts @@ -1,10 +1,10 @@ import { readFileSync } from 'fs'; import path from 'path'; import { describe, expect, it } from 'vitest'; -import Lexer from '@/core/lexer/lexer'; -import Parser from '@/core/parser/parser'; -import { SyntaxNodeIdGenerator } from '@/core/parser/nodes'; -import { scanTestNames } from '@tests/utils'; +import Lexer from '@/core/syntax/lexer/lexer'; +import Parser from '@/core/syntax/parser/parser'; +import { SyntaxNodeIdGenerator } from '@/core/types/nodes'; +import { scanTestNames, stripIds } from '@tests/utils'; describe('[snapshot] parser', () => { const testNames = scanTestNames(path.resolve(__dirname, './input/')); @@ -12,18 +12,21 @@ describe('[snapshot] parser', () => { testNames.forEach((testName) => { const program = readFileSync(path.resolve(__dirname, `./input/${testName}.in.dbml`), 'utf-8'); const lexer = new Lexer(program); - const nodeIdGenerator = new SyntaxNodeIdGenerator(); const output = JSON.stringify( lexer.lex().chain((tokens) => { - const parser = new Parser(program, tokens, nodeIdGenerator); + const parser = new Parser(program, tokens, new SyntaxNodeIdGenerator()); return parser.parse().map((_) => _.ast); }), (key: string, value: any) => { - if (key === 'source') return undefined; + if (key === 'source' || key === 'parent') return undefined; return value; }, 2, ); - it(testName, () => expect(output).toMatchFileSnapshot(path.resolve(__dirname, `./output/${testName}.out.json`))); + it(testName, () => { + const expectedPath = path.resolve(__dirname, `./output/${testName}.out.json`); + const expectedRaw = readFileSync(expectedPath, 'utf-8'); + expect(stripIds(output)).toBe(stripIds(expectedRaw)); + }); }); }); diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/alias_of_duplicated_names.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/alias_of_duplicated_names.out.json index cdb2d41ff..d7a0f10c2 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/alias_of_duplicated_names.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/alias_of_duplicated_names.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 6, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 5 }, "as": { "kind": "", @@ -275,8 +277,10 @@ "isInvalid": false, "start": 15, "end": 17 - } - } + }, + "parent": 3 + }, + "parent": 5 }, "body": { "id": 4, @@ -403,10 +407,10 @@ "isInvalid": false, "start": 23, "end": 24 - } + }, + "parent": 5 }, - "parent": 18, - "symbol": 1 + "parent": 18 }, { "id": 11, @@ -566,8 +570,10 @@ "isInvalid": false, "start": 34, "end": 39 - } - } + }, + "parent": 7 + }, + "parent": 11 }, "as": { "kind": "", @@ -688,8 +694,10 @@ "isInvalid": false, "start": 43, "end": 45 - } - } + }, + "parent": 9 + }, + "parent": 11 }, "body": { "id": 10, @@ -816,10 +824,10 @@ "isInvalid": false, "start": 51, "end": 52 - } + }, + "parent": 11 }, - "parent": 18, - "symbol": 2 + "parent": 18 }, { "id": 17, @@ -979,8 +987,10 @@ "isInvalid": false, "start": 62, "end": 70 - } - } + }, + "parent": 13 + }, + "parent": 17 }, "as": { "kind": "", @@ -1101,8 +1111,10 @@ "isInvalid": false, "start": 74, "end": 76 - } - } + }, + "parent": 15 + }, + "parent": 17 }, "body": { "id": 16, @@ -1291,10 +1303,10 @@ "isInvalid": false, "start": 86, "end": 87 - } + }, + "parent": 17 }, - "parent": 18, - "symbol": 3 + "parent": 18 } ], "eof": { @@ -1317,210 +1329,7 @@ "isInvalid": false, "start": 87, "end": 87 - }, - "symbol": { - "symbolTable": { - "Table:Users": { - "references": [], - "id": 2, - "symbolTable": {}, - "declaration": 11 - }, - "Table:U1": { - "references": [], - "id": 3, - "symbolTable": {}, - "declaration": 17 - }, - "Table:U2": { - "references": [], - "id": 2, - "symbolTable": {}, - "declaration": 11 - }, - "Table:Products": { - "references": [], - "id": 3, - "symbolTable": {}, - "declaration": 17 - } - }, - "id": 0, - "references": [] } }, - "errors": [ - { - "code": 3003, - "diagnostic": "Table name 'Users' already exists in schema 'public'", - "nodeOrToken": { - "id": 7, - "kind": "", - "startPos": { - "offset": 34, - "line": 4, - "column": 6 - }, - "fullStart": 34, - "endPos": { - "offset": 39, - "line": 4, - "column": 11 - }, - "fullEnd": 40, - "start": 34, - "end": 39, - "expression": { - "id": 6, - "kind": "", - "startPos": { - "offset": 34, - "line": 4, - "column": 6 - }, - "fullStart": 34, - "endPos": { - "offset": 39, - "line": 4, - "column": 11 - }, - "fullEnd": 40, - "start": 34, - "end": 39, - "variable": { - "kind": "", - "startPos": { - "offset": 34, - "line": 4, - "column": 6 - }, - "endPos": { - "offset": 39, - "line": 4, - "column": 11 - }, - "value": "Users", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 39, - "line": 4, - "column": 11 - }, - "endPos": { - "offset": 40, - "line": 4, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 39, - "end": 40 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 34, - "end": 39 - } - } - }, - "start": 34, - "end": 39, - "name": "CompileError" - }, - { - "code": 3003, - "diagnostic": "Table name 'U1' already exists", - "nodeOrToken": { - "id": 13, - "kind": "", - "startPos": { - "offset": 62, - "line": 8, - "column": 6 - }, - "fullStart": 62, - "endPos": { - "offset": 70, - "line": 8, - "column": 14 - }, - "fullEnd": 71, - "start": 62, - "end": 70, - "expression": { - "id": 12, - "kind": "", - "startPos": { - "offset": 62, - "line": 8, - "column": 6 - }, - "fullStart": 62, - "endPos": { - "offset": 70, - "line": 8, - "column": 14 - }, - "fullEnd": 71, - "start": 62, - "end": 70, - "variable": { - "kind": "", - "startPos": { - "offset": 62, - "line": 8, - "column": 6 - }, - "endPos": { - "offset": 70, - "line": 8, - "column": 14 - }, - "value": "Products", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 70, - "line": 8, - "column": 14 - }, - "endPos": { - "offset": 71, - "line": 8, - "column": 15 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 70, - "end": 71 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 62, - "end": 70 - } - } - }, - "start": 62, - "end": 70, - "name": "CompileError" - } - ] -} \ No newline at end of file + "errors": [] +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_columns.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_columns.out.json index 3b073b7bd..5c1638c2c 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_columns.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_columns.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 6, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 18 }, "body": { "id": 17, @@ -395,8 +397,10 @@ "isInvalid": false, "start": 19, "end": 21 - } - } + }, + "parent": 3 + }, + "parent": 6 }, "args": [ { @@ -475,11 +479,13 @@ "isInvalid": false, "start": 22, "end": 29 - } - } + }, + "parent": 5 + }, + "parent": 6 } ], - "symbol": 2 + "parent": 17 }, { "id": 11, @@ -659,8 +665,10 @@ "isInvalid": false, "start": 35, "end": 37 - } - } + }, + "parent": 8 + }, + "parent": 11 }, "args": [ { @@ -739,11 +747,13 @@ "isInvalid": false, "start": 38, "end": 45 - } - } + }, + "parent": 10 + }, + "parent": 11 } ], - "symbol": 3 + "parent": 17 }, { "id": 16, @@ -923,8 +933,10 @@ "isInvalid": false, "start": 51, "end": 53 - } - } + }, + "parent": 13 + }, + "parent": 16 }, "args": [ { @@ -1003,11 +1015,13 @@ "isInvalid": false, "start": 54, "end": 61 - } - } + }, + "parent": 15 + }, + "parent": 16 } ], - "symbol": 4 + "parent": 17 } ], "blockCloseBrace": { @@ -1052,10 +1066,10 @@ "isInvalid": false, "start": 63, "end": 64 - } + }, + "parent": 18 }, - "parent": 38, - "symbol": 1 + "parent": 38 }, { "id": 37, @@ -1215,8 +1229,10 @@ "isInvalid": false, "start": 81, "end": 92 - } - } + }, + "parent": 20 + }, + "parent": 37 }, "body": { "id": 36, @@ -1457,8 +1473,10 @@ "isInvalid": false, "start": 100, "end": 102 - } - } + }, + "parent": 22 + }, + "parent": 25 }, "args": [ { @@ -1537,11 +1555,13 @@ "isInvalid": false, "start": 103, "end": 106 - } - } + }, + "parent": 24 + }, + "parent": 25 } ], - "symbol": 6 + "parent": 36 }, { "id": 30, @@ -1721,8 +1741,10 @@ "isInvalid": false, "start": 112, "end": 114 - } - } + }, + "parent": 27 + }, + "parent": 30 }, "args": [ { @@ -1801,11 +1823,13 @@ "isInvalid": false, "start": 115, "end": 118 - } - } + }, + "parent": 29 + }, + "parent": 30 } ], - "symbol": 7 + "parent": 36 }, { "id": 35, @@ -1985,8 +2009,10 @@ "isInvalid": false, "start": 124, "end": 126 - } - } + }, + "parent": 32 + }, + "parent": 35 }, "args": [ { @@ -2065,11 +2091,13 @@ "isInvalid": false, "start": 127, "end": 130 - } - } + }, + "parent": 34 + }, + "parent": 35 } ], - "symbol": 8 + "parent": 36 } ], "blockCloseBrace": { @@ -2114,10 +2142,10 @@ "isInvalid": false, "start": 132, "end": 133 - } + }, + "parent": 37 }, - "parent": 38, - "symbol": 5 + "parent": 38 } ], "eof": { @@ -2140,2206 +2168,7 @@ "isInvalid": false, "start": 135, "end": 135 - }, - "symbol": { - "symbolTable": { - "Table:Users": { - "references": [], - "id": 1, - "symbolTable": { - "Column:id": { - "references": [], - "id": 2, - "declaration": 6 - } - }, - "declaration": 18 - }, - "TablePartial:userPartial": { - "references": [], - "id": 5, - "symbolTable": { - "Column:id": { - "references": [], - "id": 6, - "declaration": 25 - } - }, - "declaration": 37 - } - }, - "id": 0, - "references": [] } }, - "errors": [ - { - "code": 3023, - "diagnostic": "Duplicate column id", - "nodeOrToken": { - "id": 11, - "kind": "", - "startPos": { - "offset": 35, - "line": 2, - "column": 4 - }, - "fullStart": 31, - "endPos": { - "offset": 45, - "line": 2, - "column": 14 - }, - "fullEnd": 47, - "start": 35, - "end": 45, - "callee": { - "id": 8, - "kind": "", - "startPos": { - "offset": 35, - "line": 2, - "column": 4 - }, - "fullStart": 31, - "endPos": { - "offset": 37, - "line": 2, - "column": 6 - }, - "fullEnd": 38, - "start": 35, - "end": 37, - "expression": { - "id": 7, - "kind": "", - "startPos": { - "offset": 35, - "line": 2, - "column": 4 - }, - "fullStart": 31, - "endPos": { - "offset": 37, - "line": 2, - "column": 6 - }, - "fullEnd": 38, - "start": 35, - "end": 37, - "variable": { - "kind": "", - "startPos": { - "offset": 35, - "line": 2, - "column": 4 - }, - "endPos": { - "offset": 37, - "line": 2, - "column": 6 - }, - "value": "id", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 31, - "line": 2, - "column": 0 - }, - "endPos": { - "offset": 32, - "line": 2, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 31, - "end": 32 - }, - { - "kind": "", - "startPos": { - "offset": 32, - "line": 2, - "column": 1 - }, - "endPos": { - "offset": 33, - "line": 2, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 32, - "end": 33 - }, - { - "kind": "", - "startPos": { - "offset": 33, - "line": 2, - "column": 2 - }, - "endPos": { - "offset": 34, - "line": 2, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 33, - "end": 34 - }, - { - "kind": "", - "startPos": { - "offset": 34, - "line": 2, - "column": 3 - }, - "endPos": { - "offset": 35, - "line": 2, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 34, - "end": 35 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 37, - "line": 2, - "column": 6 - }, - "endPos": { - "offset": 38, - "line": 2, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 37, - "end": 38 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 35, - "end": 37 - } - } - }, - "args": [ - { - "id": 10, - "kind": "", - "startPos": { - "offset": 38, - "line": 2, - "column": 7 - }, - "fullStart": 38, - "endPos": { - "offset": 45, - "line": 2, - "column": 14 - }, - "fullEnd": 47, - "start": 38, - "end": 45, - "expression": { - "id": 9, - "kind": "", - "startPos": { - "offset": 38, - "line": 2, - "column": 7 - }, - "fullStart": 38, - "endPos": { - "offset": 45, - "line": 2, - "column": 14 - }, - "fullEnd": 47, - "start": 38, - "end": 45, - "variable": { - "kind": "", - "startPos": { - "offset": 38, - "line": 2, - "column": 7 - }, - "endPos": { - "offset": 45, - "line": 2, - "column": 14 - }, - "value": "integer", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 46, - "line": 2, - "column": 15 - }, - "endPos": { - "offset": 47, - "line": 3, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 46, - "end": 47 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 38, - "end": 45 - } - } - } - ], - "symbol": 3 - }, - "start": 35, - "end": 45, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column id", - "nodeOrToken": { - "id": 6, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 29, - "line": 1, - "column": 14 - }, - "fullEnd": 31, - "start": 19, - "end": 29, - "callee": { - "id": 3, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 21, - "line": 1, - "column": 6 - }, - "fullEnd": 22, - "start": 19, - "end": 21, - "expression": { - "id": 2, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 21, - "line": 1, - "column": 6 - }, - "fullEnd": 22, - "start": 19, - "end": 21, - "variable": { - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "endPos": { - "offset": 21, - "line": 1, - "column": 6 - }, - "value": "id", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 15, - "line": 1, - "column": 0 - }, - "endPos": { - "offset": 16, - "line": 1, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 15, - "end": 16 - }, - { - "kind": "", - "startPos": { - "offset": 16, - "line": 1, - "column": 1 - }, - "endPos": { - "offset": 17, - "line": 1, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 16, - "end": 17 - }, - { - "kind": "", - "startPos": { - "offset": 17, - "line": 1, - "column": 2 - }, - "endPos": { - "offset": 18, - "line": 1, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 17, - "end": 18 - }, - { - "kind": "", - "startPos": { - "offset": 18, - "line": 1, - "column": 3 - }, - "endPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 18, - "end": 19 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 21, - "line": 1, - "column": 6 - }, - "endPos": { - "offset": 22, - "line": 1, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 21, - "end": 22 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 19, - "end": 21 - } - } - }, - "args": [ - { - "id": 5, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 7 - }, - "fullStart": 22, - "endPos": { - "offset": 29, - "line": 1, - "column": 14 - }, - "fullEnd": 31, - "start": 22, - "end": 29, - "expression": { - "id": 4, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 7 - }, - "fullStart": 22, - "endPos": { - "offset": 29, - "line": 1, - "column": 14 - }, - "fullEnd": 31, - "start": 22, - "end": 29, - "variable": { - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 7 - }, - "endPos": { - "offset": 29, - "line": 1, - "column": 14 - }, - "value": "integer", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 30, - "line": 1, - "column": 15 - }, - "endPos": { - "offset": 31, - "line": 2, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 30, - "end": 31 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 22, - "end": 29 - } - } - } - ], - "symbol": 2 - }, - "start": 19, - "end": 29, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column id", - "nodeOrToken": { - "id": 16, - "kind": "", - "startPos": { - "offset": 51, - "line": 3, - "column": 4 - }, - "fullStart": 47, - "endPos": { - "offset": 61, - "line": 3, - "column": 14 - }, - "fullEnd": 63, - "start": 51, - "end": 61, - "callee": { - "id": 13, - "kind": "", - "startPos": { - "offset": 51, - "line": 3, - "column": 4 - }, - "fullStart": 47, - "endPos": { - "offset": 53, - "line": 3, - "column": 6 - }, - "fullEnd": 54, - "start": 51, - "end": 53, - "expression": { - "id": 12, - "kind": "", - "startPos": { - "offset": 51, - "line": 3, - "column": 4 - }, - "fullStart": 47, - "endPos": { - "offset": 53, - "line": 3, - "column": 6 - }, - "fullEnd": 54, - "start": 51, - "end": 53, - "variable": { - "kind": "", - "startPos": { - "offset": 51, - "line": 3, - "column": 4 - }, - "endPos": { - "offset": 53, - "line": 3, - "column": 6 - }, - "value": "id", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 47, - "line": 3, - "column": 0 - }, - "endPos": { - "offset": 48, - "line": 3, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 47, - "end": 48 - }, - { - "kind": "", - "startPos": { - "offset": 48, - "line": 3, - "column": 1 - }, - "endPos": { - "offset": 49, - "line": 3, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 48, - "end": 49 - }, - { - "kind": "", - "startPos": { - "offset": 49, - "line": 3, - "column": 2 - }, - "endPos": { - "offset": 50, - "line": 3, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 49, - "end": 50 - }, - { - "kind": "", - "startPos": { - "offset": 50, - "line": 3, - "column": 3 - }, - "endPos": { - "offset": 51, - "line": 3, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 50, - "end": 51 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 53, - "line": 3, - "column": 6 - }, - "endPos": { - "offset": 54, - "line": 3, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 53, - "end": 54 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 51, - "end": 53 - } - } - }, - "args": [ - { - "id": 15, - "kind": "", - "startPos": { - "offset": 54, - "line": 3, - "column": 7 - }, - "fullStart": 54, - "endPos": { - "offset": 61, - "line": 3, - "column": 14 - }, - "fullEnd": 63, - "start": 54, - "end": 61, - "expression": { - "id": 14, - "kind": "", - "startPos": { - "offset": 54, - "line": 3, - "column": 7 - }, - "fullStart": 54, - "endPos": { - "offset": 61, - "line": 3, - "column": 14 - }, - "fullEnd": 63, - "start": 54, - "end": 61, - "variable": { - "kind": "", - "startPos": { - "offset": 54, - "line": 3, - "column": 7 - }, - "endPos": { - "offset": 61, - "line": 3, - "column": 14 - }, - "value": "integer", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 62, - "line": 3, - "column": 15 - }, - "endPos": { - "offset": 63, - "line": 4, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 62, - "end": 63 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 54, - "end": 61 - } - } - } - ], - "symbol": 4 - }, - "start": 51, - "end": 61, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column id", - "nodeOrToken": { - "id": 6, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 29, - "line": 1, - "column": 14 - }, - "fullEnd": 31, - "start": 19, - "end": 29, - "callee": { - "id": 3, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 21, - "line": 1, - "column": 6 - }, - "fullEnd": 22, - "start": 19, - "end": 21, - "expression": { - "id": 2, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 21, - "line": 1, - "column": 6 - }, - "fullEnd": 22, - "start": 19, - "end": 21, - "variable": { - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "endPos": { - "offset": 21, - "line": 1, - "column": 6 - }, - "value": "id", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 15, - "line": 1, - "column": 0 - }, - "endPos": { - "offset": 16, - "line": 1, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 15, - "end": 16 - }, - { - "kind": "", - "startPos": { - "offset": 16, - "line": 1, - "column": 1 - }, - "endPos": { - "offset": 17, - "line": 1, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 16, - "end": 17 - }, - { - "kind": "", - "startPos": { - "offset": 17, - "line": 1, - "column": 2 - }, - "endPos": { - "offset": 18, - "line": 1, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 17, - "end": 18 - }, - { - "kind": "", - "startPos": { - "offset": 18, - "line": 1, - "column": 3 - }, - "endPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 18, - "end": 19 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 21, - "line": 1, - "column": 6 - }, - "endPos": { - "offset": 22, - "line": 1, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 21, - "end": 22 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 19, - "end": 21 - } - } - }, - "args": [ - { - "id": 5, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 7 - }, - "fullStart": 22, - "endPos": { - "offset": 29, - "line": 1, - "column": 14 - }, - "fullEnd": 31, - "start": 22, - "end": 29, - "expression": { - "id": 4, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 7 - }, - "fullStart": 22, - "endPos": { - "offset": 29, - "line": 1, - "column": 14 - }, - "fullEnd": 31, - "start": 22, - "end": 29, - "variable": { - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 7 - }, - "endPos": { - "offset": 29, - "line": 1, - "column": 14 - }, - "value": "integer", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 30, - "line": 1, - "column": 15 - }, - "endPos": { - "offset": 31, - "line": 2, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 30, - "end": 31 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 22, - "end": 29 - } - } - } - ], - "symbol": 2 - }, - "start": 19, - "end": 29, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column id", - "nodeOrToken": { - "id": 30, - "kind": "", - "startPos": { - "offset": 112, - "line": 8, - "column": 4 - }, - "fullStart": 108, - "endPos": { - "offset": 118, - "line": 8, - "column": 10 - }, - "fullEnd": 120, - "start": 112, - "end": 118, - "callee": { - "id": 27, - "kind": "", - "startPos": { - "offset": 112, - "line": 8, - "column": 4 - }, - "fullStart": 108, - "endPos": { - "offset": 114, - "line": 8, - "column": 6 - }, - "fullEnd": 115, - "start": 112, - "end": 114, - "expression": { - "id": 26, - "kind": "", - "startPos": { - "offset": 112, - "line": 8, - "column": 4 - }, - "fullStart": 108, - "endPos": { - "offset": 114, - "line": 8, - "column": 6 - }, - "fullEnd": 115, - "start": 112, - "end": 114, - "variable": { - "kind": "", - "startPos": { - "offset": 112, - "line": 8, - "column": 4 - }, - "endPos": { - "offset": 114, - "line": 8, - "column": 6 - }, - "value": "id", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 108, - "line": 8, - "column": 0 - }, - "endPos": { - "offset": 109, - "line": 8, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 108, - "end": 109 - }, - { - "kind": "", - "startPos": { - "offset": 109, - "line": 8, - "column": 1 - }, - "endPos": { - "offset": 110, - "line": 8, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 109, - "end": 110 - }, - { - "kind": "", - "startPos": { - "offset": 110, - "line": 8, - "column": 2 - }, - "endPos": { - "offset": 111, - "line": 8, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 110, - "end": 111 - }, - { - "kind": "", - "startPos": { - "offset": 111, - "line": 8, - "column": 3 - }, - "endPos": { - "offset": 112, - "line": 8, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 111, - "end": 112 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 114, - "line": 8, - "column": 6 - }, - "endPos": { - "offset": 115, - "line": 8, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 114, - "end": 115 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 112, - "end": 114 - } - } - }, - "args": [ - { - "id": 29, - "kind": "", - "startPos": { - "offset": 115, - "line": 8, - "column": 7 - }, - "fullStart": 115, - "endPos": { - "offset": 118, - "line": 8, - "column": 10 - }, - "fullEnd": 120, - "start": 115, - "end": 118, - "expression": { - "id": 28, - "kind": "", - "startPos": { - "offset": 115, - "line": 8, - "column": 7 - }, - "fullStart": 115, - "endPos": { - "offset": 118, - "line": 8, - "column": 10 - }, - "fullEnd": 120, - "start": 115, - "end": 118, - "variable": { - "kind": "", - "startPos": { - "offset": 115, - "line": 8, - "column": 7 - }, - "endPos": { - "offset": 118, - "line": 8, - "column": 10 - }, - "value": "int", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 119, - "line": 8, - "column": 11 - }, - "endPos": { - "offset": 120, - "line": 9, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 119, - "end": 120 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 115, - "end": 118 - } - } - } - ], - "symbol": 7 - }, - "start": 112, - "end": 118, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column id", - "nodeOrToken": { - "id": 25, - "kind": "", - "startPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "fullStart": 96, - "endPos": { - "offset": 106, - "line": 7, - "column": 10 - }, - "fullEnd": 108, - "start": 100, - "end": 106, - "callee": { - "id": 22, - "kind": "", - "startPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "fullStart": 96, - "endPos": { - "offset": 102, - "line": 7, - "column": 6 - }, - "fullEnd": 103, - "start": 100, - "end": 102, - "expression": { - "id": 21, - "kind": "", - "startPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "fullStart": 96, - "endPos": { - "offset": 102, - "line": 7, - "column": 6 - }, - "fullEnd": 103, - "start": 100, - "end": 102, - "variable": { - "kind": "", - "startPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "endPos": { - "offset": 102, - "line": 7, - "column": 6 - }, - "value": "id", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 96, - "line": 7, - "column": 0 - }, - "endPos": { - "offset": 97, - "line": 7, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 96, - "end": 97 - }, - { - "kind": "", - "startPos": { - "offset": 97, - "line": 7, - "column": 1 - }, - "endPos": { - "offset": 98, - "line": 7, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 97, - "end": 98 - }, - { - "kind": "", - "startPos": { - "offset": 98, - "line": 7, - "column": 2 - }, - "endPos": { - "offset": 99, - "line": 7, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 98, - "end": 99 - }, - { - "kind": "", - "startPos": { - "offset": 99, - "line": 7, - "column": 3 - }, - "endPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 99, - "end": 100 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 102, - "line": 7, - "column": 6 - }, - "endPos": { - "offset": 103, - "line": 7, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 102, - "end": 103 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 100, - "end": 102 - } - } - }, - "args": [ - { - "id": 24, - "kind": "", - "startPos": { - "offset": 103, - "line": 7, - "column": 7 - }, - "fullStart": 103, - "endPos": { - "offset": 106, - "line": 7, - "column": 10 - }, - "fullEnd": 108, - "start": 103, - "end": 106, - "expression": { - "id": 23, - "kind": "", - "startPos": { - "offset": 103, - "line": 7, - "column": 7 - }, - "fullStart": 103, - "endPos": { - "offset": 106, - "line": 7, - "column": 10 - }, - "fullEnd": 108, - "start": 103, - "end": 106, - "variable": { - "kind": "", - "startPos": { - "offset": 103, - "line": 7, - "column": 7 - }, - "endPos": { - "offset": 106, - "line": 7, - "column": 10 - }, - "value": "int", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 107, - "line": 7, - "column": 11 - }, - "endPos": { - "offset": 108, - "line": 8, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 107, - "end": 108 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 103, - "end": 106 - } - } - } - ], - "symbol": 6 - }, - "start": 100, - "end": 106, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column id", - "nodeOrToken": { - "id": 35, - "kind": "", - "startPos": { - "offset": 124, - "line": 9, - "column": 4 - }, - "fullStart": 120, - "endPos": { - "offset": 130, - "line": 9, - "column": 10 - }, - "fullEnd": 132, - "start": 124, - "end": 130, - "callee": { - "id": 32, - "kind": "", - "startPos": { - "offset": 124, - "line": 9, - "column": 4 - }, - "fullStart": 120, - "endPos": { - "offset": 126, - "line": 9, - "column": 6 - }, - "fullEnd": 127, - "start": 124, - "end": 126, - "expression": { - "id": 31, - "kind": "", - "startPos": { - "offset": 124, - "line": 9, - "column": 4 - }, - "fullStart": 120, - "endPos": { - "offset": 126, - "line": 9, - "column": 6 - }, - "fullEnd": 127, - "start": 124, - "end": 126, - "variable": { - "kind": "", - "startPos": { - "offset": 124, - "line": 9, - "column": 4 - }, - "endPos": { - "offset": 126, - "line": 9, - "column": 6 - }, - "value": "id", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 120, - "line": 9, - "column": 0 - }, - "endPos": { - "offset": 121, - "line": 9, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 120, - "end": 121 - }, - { - "kind": "", - "startPos": { - "offset": 121, - "line": 9, - "column": 1 - }, - "endPos": { - "offset": 122, - "line": 9, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 121, - "end": 122 - }, - { - "kind": "", - "startPos": { - "offset": 122, - "line": 9, - "column": 2 - }, - "endPos": { - "offset": 123, - "line": 9, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 122, - "end": 123 - }, - { - "kind": "", - "startPos": { - "offset": 123, - "line": 9, - "column": 3 - }, - "endPos": { - "offset": 124, - "line": 9, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 123, - "end": 124 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 126, - "line": 9, - "column": 6 - }, - "endPos": { - "offset": 127, - "line": 9, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 126, - "end": 127 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 124, - "end": 126 - } - } - }, - "args": [ - { - "id": 34, - "kind": "", - "startPos": { - "offset": 127, - "line": 9, - "column": 7 - }, - "fullStart": 127, - "endPos": { - "offset": 130, - "line": 9, - "column": 10 - }, - "fullEnd": 132, - "start": 127, - "end": 130, - "expression": { - "id": 33, - "kind": "", - "startPos": { - "offset": 127, - "line": 9, - "column": 7 - }, - "fullStart": 127, - "endPos": { - "offset": 130, - "line": 9, - "column": 10 - }, - "fullEnd": 132, - "start": 127, - "end": 130, - "variable": { - "kind": "", - "startPos": { - "offset": 127, - "line": 9, - "column": 7 - }, - "endPos": { - "offset": 130, - "line": 9, - "column": 10 - }, - "value": "int", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 131, - "line": 9, - "column": 11 - }, - "endPos": { - "offset": 132, - "line": 10, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 131, - "end": 132 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 127, - "end": 130 - } - } - } - ], - "symbol": 8 - }, - "start": 124, - "end": 130, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column id", - "nodeOrToken": { - "id": 25, - "kind": "", - "startPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "fullStart": 96, - "endPos": { - "offset": 106, - "line": 7, - "column": 10 - }, - "fullEnd": 108, - "start": 100, - "end": 106, - "callee": { - "id": 22, - "kind": "", - "startPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "fullStart": 96, - "endPos": { - "offset": 102, - "line": 7, - "column": 6 - }, - "fullEnd": 103, - "start": 100, - "end": 102, - "expression": { - "id": 21, - "kind": "", - "startPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "fullStart": 96, - "endPos": { - "offset": 102, - "line": 7, - "column": 6 - }, - "fullEnd": 103, - "start": 100, - "end": 102, - "variable": { - "kind": "", - "startPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "endPos": { - "offset": 102, - "line": 7, - "column": 6 - }, - "value": "id", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 96, - "line": 7, - "column": 0 - }, - "endPos": { - "offset": 97, - "line": 7, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 96, - "end": 97 - }, - { - "kind": "", - "startPos": { - "offset": 97, - "line": 7, - "column": 1 - }, - "endPos": { - "offset": 98, - "line": 7, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 97, - "end": 98 - }, - { - "kind": "", - "startPos": { - "offset": 98, - "line": 7, - "column": 2 - }, - "endPos": { - "offset": 99, - "line": 7, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 98, - "end": 99 - }, - { - "kind": "", - "startPos": { - "offset": 99, - "line": 7, - "column": 3 - }, - "endPos": { - "offset": 100, - "line": 7, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 99, - "end": 100 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 102, - "line": 7, - "column": 6 - }, - "endPos": { - "offset": 103, - "line": 7, - "column": 7 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 102, - "end": 103 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 100, - "end": 102 - } - } - }, - "args": [ - { - "id": 24, - "kind": "", - "startPos": { - "offset": 103, - "line": 7, - "column": 7 - }, - "fullStart": 103, - "endPos": { - "offset": 106, - "line": 7, - "column": 10 - }, - "fullEnd": 108, - "start": 103, - "end": 106, - "expression": { - "id": 23, - "kind": "", - "startPos": { - "offset": 103, - "line": 7, - "column": 7 - }, - "fullStart": 103, - "endPos": { - "offset": 106, - "line": 7, - "column": 10 - }, - "fullEnd": 108, - "start": 103, - "end": 106, - "variable": { - "kind": "", - "startPos": { - "offset": 103, - "line": 7, - "column": 7 - }, - "endPos": { - "offset": 106, - "line": 7, - "column": 10 - }, - "value": "int", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 107, - "line": 7, - "column": 11 - }, - "endPos": { - "offset": 108, - "line": 8, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 107, - "end": 108 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 103, - "end": 106 - } - } - } - ], - "symbol": 6 - }, - "start": 100, - "end": 106, - "name": "CompileError" - } - ] -} \ No newline at end of file + "errors": [] +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_enum_field.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_enum_field.out.json index 9f7056ea3..7d643c659 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_enum_field.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_enum_field.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 5, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 12 }, "body": { "id": 11, @@ -395,11 +397,13 @@ "isInvalid": false, "start": 19, "end": 24 - } - } + }, + "parent": 3 + }, + "parent": 4 }, "args": [], - "symbol": 2 + "parent": 11 }, { "id": 7, @@ -579,11 +583,13 @@ "isInvalid": false, "start": 30, "end": 35 - } - } + }, + "parent": 6 + }, + "parent": 7 }, "args": [], - "symbol": 3 + "parent": 11 }, { "id": 10, @@ -763,11 +769,13 @@ "isInvalid": false, "start": 41, "end": 46 - } - } + }, + "parent": 9 + }, + "parent": 10 }, "args": [], - "symbol": 4 + "parent": 11 } ], "blockCloseBrace": { @@ -790,10 +798,10 @@ "isInvalid": false, "start": 48, "end": 49 - } + }, + "parent": 12 }, - "parent": 13, - "symbol": 1 + "parent": 13 } ], "eof": { @@ -816,790 +824,7 @@ "isInvalid": false, "start": 49, "end": 49 - }, - "symbol": { - "symbolTable": { - "Enum:status": { - "references": [], - "id": 1, - "symbolTable": { - "Enum field:churn": { - "references": [], - "id": 2, - "declaration": 4 - } - }, - "declaration": 12 - } - }, - "id": 0, - "references": [] } }, - "errors": [ - { - "code": 3023, - "diagnostic": "Duplicate enum field churn", - "nodeOrToken": { - "id": 7, - "kind": "", - "startPos": { - "offset": 30, - "line": 2, - "column": 4 - }, - "fullStart": 26, - "endPos": { - "offset": 35, - "line": 2, - "column": 9 - }, - "fullEnd": 37, - "start": 30, - "end": 35, - "callee": { - "id": 6, - "kind": "", - "startPos": { - "offset": 30, - "line": 2, - "column": 4 - }, - "fullStart": 26, - "endPos": { - "offset": 35, - "line": 2, - "column": 9 - }, - "fullEnd": 37, - "start": 30, - "end": 35, - "expression": { - "id": 5, - "kind": "", - "startPos": { - "offset": 30, - "line": 2, - "column": 4 - }, - "fullStart": 26, - "endPos": { - "offset": 35, - "line": 2, - "column": 9 - }, - "fullEnd": 37, - "start": 30, - "end": 35, - "variable": { - "kind": "", - "startPos": { - "offset": 30, - "line": 2, - "column": 4 - }, - "endPos": { - "offset": 35, - "line": 2, - "column": 9 - }, - "value": "churn", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 26, - "line": 2, - "column": 0 - }, - "endPos": { - "offset": 27, - "line": 2, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 26, - "end": 27 - }, - { - "kind": "", - "startPos": { - "offset": 27, - "line": 2, - "column": 1 - }, - "endPos": { - "offset": 28, - "line": 2, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 27, - "end": 28 - }, - { - "kind": "", - "startPos": { - "offset": 28, - "line": 2, - "column": 2 - }, - "endPos": { - "offset": 29, - "line": 2, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 28, - "end": 29 - }, - { - "kind": "", - "startPos": { - "offset": 29, - "line": 2, - "column": 3 - }, - "endPos": { - "offset": 30, - "line": 2, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 29, - "end": 30 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 36, - "line": 2, - "column": 10 - }, - "endPos": { - "offset": 37, - "line": 3, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 36, - "end": 37 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 30, - "end": 35 - } - } - }, - "args": [], - "symbol": 3 - }, - "start": 30, - "end": 35, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate enum field churn", - "nodeOrToken": { - "id": 4, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 24, - "line": 1, - "column": 9 - }, - "fullEnd": 26, - "start": 19, - "end": 24, - "callee": { - "id": 3, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 24, - "line": 1, - "column": 9 - }, - "fullEnd": 26, - "start": 19, - "end": 24, - "expression": { - "id": 2, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 24, - "line": 1, - "column": 9 - }, - "fullEnd": 26, - "start": 19, - "end": 24, - "variable": { - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "endPos": { - "offset": 24, - "line": 1, - "column": 9 - }, - "value": "churn", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 15, - "line": 1, - "column": 0 - }, - "endPos": { - "offset": 16, - "line": 1, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 15, - "end": 16 - }, - { - "kind": "", - "startPos": { - "offset": 16, - "line": 1, - "column": 1 - }, - "endPos": { - "offset": 17, - "line": 1, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 16, - "end": 17 - }, - { - "kind": "", - "startPos": { - "offset": 17, - "line": 1, - "column": 2 - }, - "endPos": { - "offset": 18, - "line": 1, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 17, - "end": 18 - }, - { - "kind": "", - "startPos": { - "offset": 18, - "line": 1, - "column": 3 - }, - "endPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 18, - "end": 19 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 25, - "line": 1, - "column": 10 - }, - "endPos": { - "offset": 26, - "line": 2, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 25, - "end": 26 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 19, - "end": 24 - } - } - }, - "args": [], - "symbol": 2 - }, - "start": 19, - "end": 24, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate enum field churn", - "nodeOrToken": { - "id": 10, - "kind": "", - "startPos": { - "offset": 41, - "line": 3, - "column": 4 - }, - "fullStart": 37, - "endPos": { - "offset": 46, - "line": 3, - "column": 9 - }, - "fullEnd": 48, - "start": 41, - "end": 46, - "callee": { - "id": 9, - "kind": "", - "startPos": { - "offset": 41, - "line": 3, - "column": 4 - }, - "fullStart": 37, - "endPos": { - "offset": 46, - "line": 3, - "column": 9 - }, - "fullEnd": 48, - "start": 41, - "end": 46, - "expression": { - "id": 8, - "kind": "", - "startPos": { - "offset": 41, - "line": 3, - "column": 4 - }, - "fullStart": 37, - "endPos": { - "offset": 46, - "line": 3, - "column": 9 - }, - "fullEnd": 48, - "start": 41, - "end": 46, - "variable": { - "kind": "", - "startPos": { - "offset": 41, - "line": 3, - "column": 4 - }, - "endPos": { - "offset": 46, - "line": 3, - "column": 9 - }, - "value": "churn", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 37, - "line": 3, - "column": 0 - }, - "endPos": { - "offset": 38, - "line": 3, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 37, - "end": 38 - }, - { - "kind": "", - "startPos": { - "offset": 38, - "line": 3, - "column": 1 - }, - "endPos": { - "offset": 39, - "line": 3, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 38, - "end": 39 - }, - { - "kind": "", - "startPos": { - "offset": 39, - "line": 3, - "column": 2 - }, - "endPos": { - "offset": 40, - "line": 3, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 39, - "end": 40 - }, - { - "kind": "", - "startPos": { - "offset": 40, - "line": 3, - "column": 3 - }, - "endPos": { - "offset": 41, - "line": 3, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 40, - "end": 41 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 47, - "line": 3, - "column": 10 - }, - "endPos": { - "offset": 48, - "line": 4, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 47, - "end": 48 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 41, - "end": 46 - } - } - }, - "args": [], - "symbol": 4 - }, - "start": 41, - "end": 46, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate enum field churn", - "nodeOrToken": { - "id": 4, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 24, - "line": 1, - "column": 9 - }, - "fullEnd": 26, - "start": 19, - "end": 24, - "callee": { - "id": 3, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 24, - "line": 1, - "column": 9 - }, - "fullEnd": 26, - "start": 19, - "end": 24, - "expression": { - "id": 2, - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "fullStart": 15, - "endPos": { - "offset": 24, - "line": 1, - "column": 9 - }, - "fullEnd": 26, - "start": 19, - "end": 24, - "variable": { - "kind": "", - "startPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "endPos": { - "offset": 24, - "line": 1, - "column": 9 - }, - "value": "churn", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 15, - "line": 1, - "column": 0 - }, - "endPos": { - "offset": 16, - "line": 1, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 15, - "end": 16 - }, - { - "kind": "", - "startPos": { - "offset": 16, - "line": 1, - "column": 1 - }, - "endPos": { - "offset": 17, - "line": 1, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 16, - "end": 17 - }, - { - "kind": "", - "startPos": { - "offset": 17, - "line": 1, - "column": 2 - }, - "endPos": { - "offset": 18, - "line": 1, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 17, - "end": 18 - }, - { - "kind": "", - "startPos": { - "offset": 18, - "line": 1, - "column": 3 - }, - "endPos": { - "offset": 19, - "line": 1, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 18, - "end": 19 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 25, - "line": 1, - "column": 10 - }, - "endPos": { - "offset": 26, - "line": 2, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 25, - "end": 26 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 19, - "end": 24 - } - } - }, - "args": [], - "symbol": 2 - }, - "start": 19, - "end": 24, - "name": "CompileError" - } - ] -} \ No newline at end of file + "errors": [] +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_names.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_names.out.json index e23e4d4f2..f8f6d1ba8 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_names.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_names.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 6, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 8 }, "body": { "id": 7, @@ -353,8 +355,10 @@ "isInvalid": false, "start": 17, "end": 19 - } - } + }, + "parent": 3 + }, + "parent": 6 }, "args": [ { @@ -433,11 +437,13 @@ "isInvalid": false, "start": 20, "end": 27 - } - } + }, + "parent": 5 + }, + "parent": 6 } ], - "symbol": 2 + "parent": 7 } ], "blockCloseBrace": { @@ -482,10 +488,10 @@ "isInvalid": false, "start": 29, "end": 30 - } + }, + "parent": 8 }, - "parent": 46, - "symbol": 1 + "parent": 46 }, { "id": 21, @@ -645,8 +651,10 @@ "isInvalid": false, "start": 40, "end": 45 - } - } + }, + "parent": 10 + }, + "parent": 21 }, "body": { "id": 20, @@ -845,8 +853,10 @@ "isInvalid": false, "start": 51, "end": 55 - } - } + }, + "parent": 12 + }, + "parent": 19 }, "args": [ { @@ -920,8 +930,10 @@ "isInvalid": false, "start": 56, "end": 60 - } - } + }, + "parent": 14 + }, + "parent": 18 }, "argumentList": { "id": 17, @@ -1016,8 +1028,10 @@ "isInvalid": false, "start": 61, "end": 64 - } - } + }, + "parent": 16 + }, + "parent": 17 } ], "commaList": [], @@ -1063,11 +1077,13 @@ "isInvalid": false, "start": 64, "end": 65 - } - } + }, + "parent": 18 + }, + "parent": 19 } ], - "symbol": 4 + "parent": 20 } ], "blockCloseBrace": { @@ -1112,10 +1128,10 @@ "isInvalid": false, "start": 67, "end": 68 - } + }, + "parent": 21 }, - "parent": 46, - "symbol": 3 + "parent": 46 }, { "id": 25, @@ -1275,8 +1291,10 @@ "isInvalid": false, "start": 83, "end": 88 - } - } + }, + "parent": 23 + }, + "parent": 25 }, "body": { "id": 24, @@ -1403,10 +1421,10 @@ "isInvalid": false, "start": 94, "end": 95 - } + }, + "parent": 25 }, - "parent": 46, - "symbol": 5 + "parent": 46 }, { "id": 29, @@ -1566,8 +1584,10 @@ "isInvalid": false, "start": 110, "end": 115 - } - } + }, + "parent": 27 + }, + "parent": 29 }, "body": { "id": 28, @@ -1694,10 +1714,10 @@ "isInvalid": false, "start": 121, "end": 122 - } + }, + "parent": 29 }, - "parent": 46, - "symbol": 6 + "parent": 46 }, { "id": 33, @@ -1857,8 +1877,10 @@ "isInvalid": false, "start": 131, "end": 136 - } - } + }, + "parent": 31 + }, + "parent": 33 }, "body": { "id": 32, @@ -1985,10 +2007,10 @@ "isInvalid": false, "start": 142, "end": 143 - } + }, + "parent": 33 }, - "parent": 46, - "symbol": 7 + "parent": 46 }, { "id": 37, @@ -2148,8 +2170,10 @@ "isInvalid": false, "start": 152, "end": 157 - } - } + }, + "parent": 35 + }, + "parent": 37 }, "body": { "id": 36, @@ -2276,10 +2300,10 @@ "isInvalid": false, "start": 163, "end": 164 - } + }, + "parent": 37 }, - "parent": 46, - "symbol": 8 + "parent": 46 }, { "id": 41, @@ -2439,8 +2463,10 @@ "isInvalid": false, "start": 181, "end": 186 - } - } + }, + "parent": 39 + }, + "parent": 41 }, "body": { "id": 40, @@ -2567,10 +2593,10 @@ "isInvalid": false, "start": 192, "end": 193 - } + }, + "parent": 41 }, - "parent": 46, - "symbol": 9 + "parent": 46 }, { "id": 45, @@ -2730,8 +2756,10 @@ "isInvalid": false, "start": 210, "end": 215 - } - } + }, + "parent": 43 + }, + "parent": 45 }, "body": { "id": 44, @@ -2858,10 +2886,10 @@ "isInvalid": false, "start": 221, "end": 222 - } + }, + "parent": 45 }, - "parent": 46, - "symbol": 10 + "parent": 46 } ], "eof": { @@ -2884,217 +2912,9 @@ "isInvalid": false, "start": 224, "end": 224 - }, - "symbol": { - "symbolTable": { - "Table:Users": { - "references": [], - "id": 3, - "symbolTable": { - "Column:name": { - "references": [], - "id": 4, - "declaration": 19 - } - }, - "declaration": 21 - }, - "TableGroup:Users": { - "references": [], - "id": 5, - "symbolTable": {}, - "declaration": 25 - }, - "Enum:Users": { - "references": [], - "id": 8, - "symbolTable": {}, - "declaration": 37 - }, - "TablePartial:Users": { - "references": [], - "id": 9, - "symbolTable": {}, - "declaration": 41 - } - }, - "id": 0, - "references": [] } }, "errors": [ - { - "code": 3003, - "diagnostic": "Table name 'Users' already exists in schema 'public'", - "nodeOrToken": { - "id": 10, - "kind": "", - "startPos": { - "offset": 40, - "line": 4, - "column": 6 - }, - "fullStart": 40, - "endPos": { - "offset": 45, - "line": 4, - "column": 11 - }, - "fullEnd": 46, - "start": 40, - "end": 45, - "expression": { - "id": 9, - "kind": "", - "startPos": { - "offset": 40, - "line": 4, - "column": 6 - }, - "fullStart": 40, - "endPos": { - "offset": 45, - "line": 4, - "column": 11 - }, - "fullEnd": 46, - "start": 40, - "end": 45, - "variable": { - "kind": "", - "startPos": { - "offset": 40, - "line": 4, - "column": 6 - }, - "endPos": { - "offset": 45, - "line": 4, - "column": 11 - }, - "value": "Users", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 45, - "line": 4, - "column": 11 - }, - "endPos": { - "offset": 46, - "line": 4, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 45, - "end": 46 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 40, - "end": 45 - } - } - }, - "start": 40, - "end": 45, - "name": "CompileError" - }, - { - "code": 3003, - "diagnostic": "TableGroup name 'Users' already exists", - "nodeOrToken": { - "id": 27, - "kind": "", - "startPos": { - "offset": 110, - "line": 12, - "column": 11 - }, - "fullStart": 110, - "endPos": { - "offset": 115, - "line": 12, - "column": 16 - }, - "fullEnd": 116, - "start": 110, - "end": 115, - "expression": { - "id": 26, - "kind": "", - "startPos": { - "offset": 110, - "line": 12, - "column": 11 - }, - "fullStart": 110, - "endPos": { - "offset": 115, - "line": 12, - "column": 16 - }, - "fullEnd": 116, - "start": 110, - "end": 115, - "variable": { - "kind": "", - "startPos": { - "offset": 110, - "line": 12, - "column": 11 - }, - "endPos": { - "offset": 115, - "line": 12, - "column": 16 - }, - "value": "Users", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 115, - "line": 12, - "column": 16 - }, - "endPos": { - "offset": 116, - "line": 12, - "column": 17 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 115, - "end": 116 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 110, - "end": 115 - } - } - }, - "start": 110, - "end": 115, - "name": "CompileError" - }, { "code": 3033, "diagnostic": "An Enum must have at least one element", @@ -3256,8 +3076,10 @@ "isInvalid": false, "start": 131, "end": 136 - } - } + }, + "parent": 31 + }, + "parent": 33 }, "body": { "id": 32, @@ -3384,101 +3206,15 @@ "isInvalid": false, "start": 142, "end": 143 - } + }, + "parent": 33 }, - "parent": 46, - "symbol": 7 + "parent": 46 }, "start": 126, "end": 143, "name": "CompileError" }, - { - "code": 3003, - "diagnostic": "Enum name Users already exists in schema 'public'", - "nodeOrToken": { - "id": 35, - "kind": "", - "startPos": { - "offset": 152, - "line": 20, - "column": 5 - }, - "fullStart": 152, - "endPos": { - "offset": 157, - "line": 20, - "column": 10 - }, - "fullEnd": 158, - "start": 152, - "end": 157, - "expression": { - "id": 34, - "kind": "", - "startPos": { - "offset": 152, - "line": 20, - "column": 5 - }, - "fullStart": 152, - "endPos": { - "offset": 157, - "line": 20, - "column": 10 - }, - "fullEnd": 158, - "start": 152, - "end": 157, - "variable": { - "kind": "", - "startPos": { - "offset": 152, - "line": 20, - "column": 5 - }, - "endPos": { - "offset": 157, - "line": 20, - "column": 10 - }, - "value": "Users", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 157, - "line": 20, - "column": 10 - }, - "endPos": { - "offset": 158, - "line": 20, - "column": 11 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 157, - "end": 158 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 152, - "end": 157 - } - } - }, - "start": 152, - "end": 157, - "name": "CompileError" - }, { "code": 3033, "diagnostic": "An Enum must have at least one element", @@ -3640,8 +3376,10 @@ "isInvalid": false, "start": 152, "end": 157 - } - } + }, + "parent": 35 + }, + "parent": 37 }, "body": { "id": 36, @@ -3768,100 +3506,14 @@ "isInvalid": false, "start": 163, "end": 164 - } + }, + "parent": 37 }, - "parent": 46, - "symbol": 8 + "parent": 46 }, "start": 147, "end": 164, "name": "CompileError" - }, - { - "code": 3003, - "diagnostic": "TablePartial name 'Users' already exists", - "nodeOrToken": { - "id": 43, - "kind": "", - "startPos": { - "offset": 210, - "line": 28, - "column": 13 - }, - "fullStart": 210, - "endPos": { - "offset": 215, - "line": 28, - "column": 18 - }, - "fullEnd": 216, - "start": 210, - "end": 215, - "expression": { - "id": 42, - "kind": "", - "startPos": { - "offset": 210, - "line": 28, - "column": 13 - }, - "fullStart": 210, - "endPos": { - "offset": 215, - "line": 28, - "column": 18 - }, - "fullEnd": 216, - "start": 210, - "end": 215, - "variable": { - "kind": "", - "startPos": { - "offset": 210, - "line": 28, - "column": 13 - }, - "endPos": { - "offset": 215, - "line": 28, - "column": 18 - }, - "value": "Users", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 215, - "line": 28, - "column": 18 - }, - "endPos": { - "offset": 216, - "line": 28, - "column": 19 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 215, - "end": 216 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 210, - "end": 215 - } - } - }, - "start": 210, - "end": 215, - "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_table_partial_injections.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_table_partial_injections.out.json index 6e03d5e2d..36d9e5cb9 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_table_partial_injections.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/duplicate_table_partial_injections.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 13, "end": 19 - } - } + }, + "parent": 1 + }, + "parent": 8 }, "body": { "id": 7, @@ -353,8 +355,10 @@ "isInvalid": false, "start": 25, "end": 29 - } - } + }, + "parent": 3 + }, + "parent": 6 }, "args": [ { @@ -433,11 +437,13 @@ "isInvalid": false, "start": 30, "end": 34 - } - } + }, + "parent": 5 + }, + "parent": 6 } ], - "symbol": 2 + "parent": 7 } ], "blockCloseBrace": { @@ -482,10 +488,10 @@ "isInvalid": false, "start": 36, "end": 37 - } + }, + "parent": 8 }, - "parent": 30, - "symbol": 1 + "parent": 30 }, { "id": 29, @@ -645,8 +651,10 @@ "isInvalid": false, "start": 47, "end": 52 - } - } + }, + "parent": 10 + }, + "parent": 29 }, "body": { "id": 28, @@ -845,8 +853,10 @@ "isInvalid": false, "start": 58, "end": 60 - } - } + }, + "parent": 12 + }, + "parent": 15 }, "args": [ { @@ -925,11 +935,13 @@ "isInvalid": false, "start": 61, "end": 64 - } - } + }, + "parent": 14 + }, + "parent": 15 } ], - "symbol": 4 + "parent": 28 }, { "id": 19, @@ -1105,11 +1117,15 @@ "isInvalid": false, "start": 69, "end": 75 - } - } - } + }, + "parent": 17 + }, + "parent": 18 + }, + "parent": 19 }, - "args": [] + "args": [], + "parent": 28 }, { "id": 23, @@ -1285,11 +1301,15 @@ "isInvalid": false, "start": 80, "end": 86 - } - } - } + }, + "parent": 21 + }, + "parent": 22 + }, + "parent": 23 }, - "args": [] + "args": [], + "parent": 28 }, { "id": 27, @@ -1465,11 +1485,15 @@ "isInvalid": false, "start": 91, "end": 97 - } - } - } + }, + "parent": 25 + }, + "parent": 26 + }, + "parent": 27 }, - "args": [] + "args": [], + "parent": 28 } ], "blockCloseBrace": { @@ -1514,10 +1538,10 @@ "isInvalid": false, "start": 99, "end": 100 - } + }, + "parent": 29 }, - "parent": 30, - "symbol": 3 + "parent": 30 } ], "eof": { @@ -1540,42 +1564,6 @@ "isInvalid": false, "start": 102, "end": 102 - }, - "symbol": { - "symbolTable": { - "TablePartial:common": { - "references": [], - "id": 1, - "symbolTable": { - "Column:name": { - "references": [], - "id": 2, - "declaration": 6 - } - }, - "declaration": 8 - }, - "Table:Users": { - "references": [], - "id": 3, - "symbolTable": { - "Column:id": { - "references": [], - "id": 4, - "declaration": 15 - }, - "PartialInjection:common": { - "references": [], - "id": 5, - "symbolTable": {}, - "declaration": 19 - } - }, - "declaration": 29 - } - }, - "id": 0, - "references": [] } }, "errors": [ @@ -1583,8 +1571,8 @@ "code": 3068, "diagnostic": "Duplicate table partial injection 'common'", "nodeOrToken": { - "id": 23, - "kind": "", + "id": 22, + "kind": "", "startPos": { "offset": 79, "line": 7, @@ -1599,90 +1587,90 @@ "fullEnd": 88, "start": 79, "end": 86, - "callee": { - "id": 22, - "kind": "", + "op": { + "kind": "", "startPos": { "offset": 79, "line": 7, "column": 2 }, - "fullStart": 77, + "endPos": { + "offset": 80, + "line": 7, + "column": 3 + }, + "value": "~", + "leadingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 77, + "line": 7, + "column": 0 + }, + "endPos": { + "offset": 78, + "line": 7, + "column": 1 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 77, + "end": 78 + }, + { + "kind": "", + "startPos": { + "offset": 78, + "line": 7, + "column": 1 + }, + "endPos": { + "offset": 79, + "line": 7, + "column": 2 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 78, + "end": 79 + } + ], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 79, + "end": 80 + }, + "expression": { + "id": 21, + "kind": "", + "startPos": { + "offset": 80, + "line": 7, + "column": 3 + }, + "fullStart": 80, "endPos": { "offset": 86, "line": 7, "column": 9 }, "fullEnd": 88, - "start": 79, + "start": 80, "end": 86, - "op": { - "kind": "", - "startPos": { - "offset": 79, - "line": 7, - "column": 2 - }, - "endPos": { - "offset": 80, - "line": 7, - "column": 3 - }, - "value": "~", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 77, - "line": 7, - "column": 0 - }, - "endPos": { - "offset": 78, - "line": 7, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 77, - "end": 78 - }, - { - "kind": "", - "startPos": { - "offset": 78, - "line": 7, - "column": 1 - }, - "endPos": { - "offset": 79, - "line": 7, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 78, - "end": 79 - } - ], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 79, - "end": 80 - }, "expression": { - "id": 21, - "kind": "", + "id": 20, + "kind": "", "startPos": { "offset": 80, "line": 7, @@ -1697,70 +1685,54 @@ "fullEnd": 88, "start": 80, "end": 86, - "expression": { - "id": 20, - "kind": "", + "variable": { + "kind": "", "startPos": { "offset": 80, "line": 7, "column": 3 }, - "fullStart": 80, "endPos": { "offset": 86, "line": 7, "column": 9 }, - "fullEnd": 88, + "value": "common", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 87, + "line": 7, + "column": 10 + }, + "endPos": { + "offset": 88, + "line": 8, + "column": 0 + }, + "value": "\n", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 87, + "end": 88 + } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, "start": 80, - "end": 86, - "variable": { - "kind": "", - "startPos": { - "offset": 80, - "line": 7, - "column": 3 - }, - "endPos": { - "offset": 86, - "line": 7, - "column": 9 - }, - "value": "common", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 87, - "line": 7, - "column": 10 - }, - "endPos": { - "offset": 88, - "line": 8, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 87, - "end": 88 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 80, - "end": 86 - } - } - } + "end": 86 + }, + "parent": 21 + }, + "parent": 22 }, - "args": [] + "parent": 23 }, "start": 79, "end": 86, @@ -1770,8 +1742,8 @@ "code": 3068, "diagnostic": "Duplicate table partial injection 'common'", "nodeOrToken": { - "id": 19, - "kind": "", + "id": 18, + "kind": "", "startPos": { "offset": 68, "line": 6, @@ -1786,168 +1758,152 @@ "fullEnd": 77, "start": 68, "end": 75, - "callee": { - "id": 18, - "kind": "", + "op": { + "kind": "", "startPos": { "offset": 68, "line": 6, "column": 2 }, - "fullStart": 66, + "endPos": { + "offset": 69, + "line": 6, + "column": 3 + }, + "value": "~", + "leadingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 66, + "line": 6, + "column": 0 + }, + "endPos": { + "offset": 67, + "line": 6, + "column": 1 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 66, + "end": 67 + }, + { + "kind": "", + "startPos": { + "offset": 67, + "line": 6, + "column": 1 + }, + "endPos": { + "offset": 68, + "line": 6, + "column": 2 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 67, + "end": 68 + } + ], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 68, + "end": 69 + }, + "expression": { + "id": 17, + "kind": "", + "startPos": { + "offset": 69, + "line": 6, + "column": 3 + }, + "fullStart": 69, "endPos": { "offset": 75, "line": 6, "column": 9 }, "fullEnd": 77, - "start": 68, + "start": 69, "end": 75, - "op": { - "kind": "", + "expression": { + "id": 16, + "kind": "", "startPos": { - "offset": 68, + "offset": 69, "line": 6, - "column": 2 + "column": 3 }, + "fullStart": 69, "endPos": { - "offset": 69, + "offset": 75, "line": 6, - "column": 3 - }, - "value": "~", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 66, - "line": 6, - "column": 0 - }, - "endPos": { - "offset": 67, - "line": 6, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 66, - "end": 67 - }, - { - "kind": "", - "startPos": { - "offset": 67, - "line": 6, - "column": 1 - }, - "endPos": { - "offset": 68, - "line": 6, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 67, - "end": 68 - } - ], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 68, - "end": 69 - }, - "expression": { - "id": 17, - "kind": "", - "startPos": { - "offset": 69, - "line": 6, - "column": 3 - }, - "fullStart": 69, - "endPos": { - "offset": 75, - "line": 6, - "column": 9 + "column": 9 }, "fullEnd": 77, "start": 69, "end": 75, - "expression": { - "id": 16, - "kind": "", + "variable": { + "kind": "", "startPos": { "offset": 69, "line": 6, "column": 3 }, - "fullStart": 69, "endPos": { "offset": 75, "line": 6, "column": 9 }, - "fullEnd": 77, + "value": "common", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 76, + "line": 6, + "column": 10 + }, + "endPos": { + "offset": 77, + "line": 7, + "column": 0 + }, + "value": "\n", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 76, + "end": 77 + } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, "start": 69, - "end": 75, - "variable": { - "kind": "", - "startPos": { - "offset": 69, - "line": 6, - "column": 3 - }, - "endPos": { - "offset": 75, - "line": 6, - "column": 9 - }, - "value": "common", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 76, - "line": 6, - "column": 10 - }, - "endPos": { - "offset": 77, - "line": 7, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 76, - "end": 77 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 69, - "end": 75 - } - } - } + "end": 75 + }, + "parent": 17 + }, + "parent": 18 }, - "args": [] + "parent": 19 }, "start": 68, "end": 75, @@ -1957,8 +1913,8 @@ "code": 3068, "diagnostic": "Duplicate table partial injection 'common'", "nodeOrToken": { - "id": 27, - "kind": "", + "id": 26, + "kind": "", "startPos": { "offset": 90, "line": 8, @@ -1973,90 +1929,90 @@ "fullEnd": 99, "start": 90, "end": 97, - "callee": { - "id": 26, - "kind": "", + "op": { + "kind": "", "startPos": { "offset": 90, "line": 8, "column": 2 }, - "fullStart": 88, + "endPos": { + "offset": 91, + "line": 8, + "column": 3 + }, + "value": "~", + "leadingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 88, + "line": 8, + "column": 0 + }, + "endPos": { + "offset": 89, + "line": 8, + "column": 1 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 88, + "end": 89 + }, + { + "kind": "", + "startPos": { + "offset": 89, + "line": 8, + "column": 1 + }, + "endPos": { + "offset": 90, + "line": 8, + "column": 2 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 89, + "end": 90 + } + ], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 90, + "end": 91 + }, + "expression": { + "id": 25, + "kind": "", + "startPos": { + "offset": 91, + "line": 8, + "column": 3 + }, + "fullStart": 91, "endPos": { "offset": 97, "line": 8, "column": 9 }, "fullEnd": 99, - "start": 90, + "start": 91, "end": 97, - "op": { - "kind": "", - "startPos": { - "offset": 90, - "line": 8, - "column": 2 - }, - "endPos": { - "offset": 91, - "line": 8, - "column": 3 - }, - "value": "~", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 88, - "line": 8, - "column": 0 - }, - "endPos": { - "offset": 89, - "line": 8, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 88, - "end": 89 - }, - { - "kind": "", - "startPos": { - "offset": 89, - "line": 8, - "column": 1 - }, - "endPos": { - "offset": 90, - "line": 8, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 89, - "end": 90 - } - ], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 90, - "end": 91 - }, "expression": { - "id": 25, - "kind": "", + "id": 24, + "kind": "", "startPos": { "offset": 91, "line": 8, @@ -2071,70 +2027,54 @@ "fullEnd": 99, "start": 91, "end": 97, - "expression": { - "id": 24, - "kind": "", + "variable": { + "kind": "", "startPos": { "offset": 91, "line": 8, "column": 3 }, - "fullStart": 91, "endPos": { "offset": 97, "line": 8, "column": 9 }, - "fullEnd": 99, + "value": "common", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 98, + "line": 8, + "column": 10 + }, + "endPos": { + "offset": 99, + "line": 9, + "column": 0 + }, + "value": "\n", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 98, + "end": 99 + } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, "start": 91, - "end": 97, - "variable": { - "kind": "", - "startPos": { - "offset": 91, - "line": 8, - "column": 3 - }, - "endPos": { - "offset": 97, - "line": 8, - "column": 9 - }, - "value": "common", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 98, - "line": 8, - "column": 10 - }, - "endPos": { - "offset": 99, - "line": 9, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 98, - "end": 99 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 91, - "end": 97 - } - } - } + "end": 97 + }, + "parent": 25 + }, + "parent": 26 }, - "args": [] + "parent": 27 }, "start": 90, "end": 97, @@ -2144,8 +2084,8 @@ "code": 3068, "diagnostic": "Duplicate table partial injection 'common'", "nodeOrToken": { - "id": 19, - "kind": "", + "id": 18, + "kind": "", "startPos": { "offset": 68, "line": 6, @@ -2160,90 +2100,90 @@ "fullEnd": 77, "start": 68, "end": 75, - "callee": { - "id": 18, - "kind": "", + "op": { + "kind": "", "startPos": { "offset": 68, "line": 6, "column": 2 }, - "fullStart": 66, + "endPos": { + "offset": 69, + "line": 6, + "column": 3 + }, + "value": "~", + "leadingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 66, + "line": 6, + "column": 0 + }, + "endPos": { + "offset": 67, + "line": 6, + "column": 1 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 66, + "end": 67 + }, + { + "kind": "", + "startPos": { + "offset": 67, + "line": 6, + "column": 1 + }, + "endPos": { + "offset": 68, + "line": 6, + "column": 2 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 67, + "end": 68 + } + ], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 68, + "end": 69 + }, + "expression": { + "id": 17, + "kind": "", + "startPos": { + "offset": 69, + "line": 6, + "column": 3 + }, + "fullStart": 69, "endPos": { "offset": 75, "line": 6, "column": 9 }, "fullEnd": 77, - "start": 68, + "start": 69, "end": 75, - "op": { - "kind": "", - "startPos": { - "offset": 68, - "line": 6, - "column": 2 - }, - "endPos": { - "offset": 69, - "line": 6, - "column": 3 - }, - "value": "~", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 66, - "line": 6, - "column": 0 - }, - "endPos": { - "offset": 67, - "line": 6, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 66, - "end": 67 - }, - { - "kind": "", - "startPos": { - "offset": 67, - "line": 6, - "column": 1 - }, - "endPos": { - "offset": 68, - "line": 6, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 67, - "end": 68 - } - ], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 68, - "end": 69 - }, "expression": { - "id": 17, - "kind": "", + "id": 16, + "kind": "", "startPos": { "offset": 69, "line": 6, @@ -2258,74 +2198,58 @@ "fullEnd": 77, "start": 69, "end": 75, - "expression": { - "id": 16, - "kind": "", + "variable": { + "kind": "", "startPos": { "offset": 69, "line": 6, "column": 3 }, - "fullStart": 69, "endPos": { "offset": 75, "line": 6, "column": 9 }, - "fullEnd": 77, + "value": "common", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 76, + "line": 6, + "column": 10 + }, + "endPos": { + "offset": 77, + "line": 7, + "column": 0 + }, + "value": "\n", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 76, + "end": 77 + } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, "start": 69, - "end": 75, - "variable": { - "kind": "", - "startPos": { - "offset": 69, - "line": 6, - "column": 3 - }, - "endPos": { - "offset": 75, - "line": 6, - "column": 9 - }, - "value": "common", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 76, - "line": 6, - "column": 10 - }, - "endPos": { - "offset": 77, - "line": 7, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 76, - "end": 77 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 69, - "end": 75 - } - } - } + "end": 75 + }, + "parent": 17 + }, + "parent": 18 }, - "args": [] + "parent": 19 }, "start": 68, "end": 75, "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/enum.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/enum.out.json index f8fb1c675..3272e3020 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/enum.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/enum.out.json @@ -169,8 +169,10 @@ "isInvalid": false, "start": 5, "end": 6 - } - } + }, + "parent": 1 + }, + "parent": 4 }, "rightExpression": { "id": 3, @@ -248,9 +250,12 @@ "isInvalid": false, "start": 7, "end": 10 - } - } - } + }, + "parent": 3 + }, + "parent": 4 + }, + "parent": 6 }, "body": { "id": 5, @@ -377,10 +382,10 @@ "isInvalid": false, "start": 16, "end": 17 - } + }, + "parent": 6 }, - "parent": 24, - "symbol": 1 + "parent": 24 }, { "id": 23, @@ -703,11 +708,13 @@ "isInvalid": false, "start": 33, "end": 36 - } - } + }, + "parent": 8 + }, + "parent": 9 }, "args": [], - "symbol": 4 + "parent": 22 }, { "id": 12, @@ -887,11 +894,13 @@ "isInvalid": false, "start": 42, "end": 45 - } - } + }, + "parent": 11 + }, + "parent": 12 }, "args": [], - "symbol": 5 + "parent": 22 }, { "id": 15, @@ -1071,10 +1080,13 @@ "isInvalid": false, "start": 51, "end": 52 - } - } + }, + "parent": 14 + }, + "parent": 15 }, - "args": [] + "args": [], + "parent": 22 }, { "id": 18, @@ -1254,11 +1266,13 @@ "isInvalid": false, "start": 58, "end": 59 - } - } + }, + "parent": 17 + }, + "parent": 18 }, "args": [], - "symbol": 6 + "parent": 22 }, { "id": 21, @@ -1438,11 +1452,13 @@ "isInvalid": false, "start": 65, "end": 69 - } - } + }, + "parent": 20 + }, + "parent": 21 }, "args": [], - "symbol": 7 + "parent": 22 } ], "blockCloseBrace": { @@ -1465,10 +1481,10 @@ "isInvalid": false, "start": 71, "end": 72 - } + }, + "parent": 23 }, - "parent": 24, - "symbol": 3 + "parent": 24 } ], "eof": { @@ -1491,24 +1507,6 @@ "isInvalid": false, "start": 72, "end": 72 - }, - "symbol": { - "symbolTable": { - "Schema:v": { - "references": [], - "id": 2, - "symbolTable": { - "Enum:A": { - "references": [], - "id": 1, - "symbolTable": {}, - "declaration": 6 - } - } - } - }, - "id": 0, - "references": [] } }, "errors": [ @@ -1667,8 +1665,10 @@ "isInvalid": false, "start": 5, "end": 6 - } - } + }, + "parent": 1 + }, + "parent": 4 }, "rightExpression": { "id": 3, @@ -1746,9 +1746,12 @@ "isInvalid": false, "start": 7, "end": 10 - } - } - } + }, + "parent": 3 + }, + "parent": 4 + }, + "parent": 6 }, "body": { "id": 5, @@ -1875,10 +1878,10 @@ "isInvalid": false, "start": 16, "end": 17 - } + }, + "parent": 6 }, - "parent": 24, - "symbol": 1 + "parent": 24 }, "start": 0, "end": 17, @@ -2208,11 +2211,13 @@ "isInvalid": false, "start": 33, "end": 36 - } - } + }, + "parent": 8 + }, + "parent": 9 }, "args": [], - "symbol": 4 + "parent": 22 }, { "id": 12, @@ -2392,11 +2397,13 @@ "isInvalid": false, "start": 42, "end": 45 - } - } + }, + "parent": 11 + }, + "parent": 12 }, "args": [], - "symbol": 5 + "parent": 22 }, { "id": 15, @@ -2576,10 +2583,13 @@ "isInvalid": false, "start": 51, "end": 52 - } - } + }, + "parent": 14 + }, + "parent": 15 }, - "args": [] + "args": [], + "parent": 22 }, { "id": 18, @@ -2759,11 +2769,13 @@ "isInvalid": false, "start": 58, "end": 59 - } - } + }, + "parent": 17 + }, + "parent": 18 }, "args": [], - "symbol": 6 + "parent": 22 }, { "id": 21, @@ -2943,11 +2955,13 @@ "isInvalid": false, "start": 65, "end": 69 - } - } + }, + "parent": 20 + }, + "parent": 21 }, "args": [], - "symbol": 7 + "parent": 22 } ], "blockCloseBrace": { @@ -2970,397 +2984,15 @@ "isInvalid": false, "start": 71, "end": 72 - } + }, + "parent": 23 }, - "parent": 24, - "symbol": 3 + "parent": 24 }, "start": 21, "end": 72, "name": "CompileError" }, - { - "code": 3023, - "diagnostic": "Duplicate enum field 1", - "nodeOrToken": { - "id": 12, - "kind": "", - "startPos": { - "offset": 42, - "line": 6, - "column": 4 - }, - "fullStart": 38, - "endPos": { - "offset": 45, - "line": 6, - "column": 7 - }, - "fullEnd": 47, - "start": 42, - "end": 45, - "callee": { - "id": 11, - "kind": "", - "startPos": { - "offset": 42, - "line": 6, - "column": 4 - }, - "fullStart": 38, - "endPos": { - "offset": 45, - "line": 6, - "column": 7 - }, - "fullEnd": 47, - "start": 42, - "end": 45, - "expression": { - "id": 10, - "kind": "", - "startPos": { - "offset": 42, - "line": 6, - "column": 4 - }, - "fullStart": 38, - "endPos": { - "offset": 45, - "line": 6, - "column": 7 - }, - "fullEnd": 47, - "start": 42, - "end": 45, - "variable": { - "kind": "", - "startPos": { - "offset": 42, - "line": 6, - "column": 4 - }, - "endPos": { - "offset": 45, - "line": 6, - "column": 7 - }, - "value": "1", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 38, - "line": 6, - "column": 0 - }, - "endPos": { - "offset": 39, - "line": 6, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 38, - "end": 39 - }, - { - "kind": "", - "startPos": { - "offset": 39, - "line": 6, - "column": 1 - }, - "endPos": { - "offset": 40, - "line": 6, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 39, - "end": 40 - }, - { - "kind": "", - "startPos": { - "offset": 40, - "line": 6, - "column": 2 - }, - "endPos": { - "offset": 41, - "line": 6, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 40, - "end": 41 - }, - { - "kind": "", - "startPos": { - "offset": 41, - "line": 6, - "column": 3 - }, - "endPos": { - "offset": 42, - "line": 6, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 41, - "end": 42 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 46, - "line": 6, - "column": 8 - }, - "endPos": { - "offset": 47, - "line": 7, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 46, - "end": 47 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 42, - "end": 45 - } - } - }, - "args": [], - "symbol": 5 - }, - "start": 42, - "end": 45, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate enum field 1", - "nodeOrToken": { - "id": 9, - "kind": "", - "startPos": { - "offset": 33, - "line": 5, - "column": 4 - }, - "fullStart": 29, - "endPos": { - "offset": 36, - "line": 5, - "column": 7 - }, - "fullEnd": 38, - "start": 33, - "end": 36, - "callee": { - "id": 8, - "kind": "", - "startPos": { - "offset": 33, - "line": 5, - "column": 4 - }, - "fullStart": 29, - "endPos": { - "offset": 36, - "line": 5, - "column": 7 - }, - "fullEnd": 38, - "start": 33, - "end": 36, - "expression": { - "id": 7, - "kind": "", - "startPos": { - "offset": 33, - "line": 5, - "column": 4 - }, - "fullStart": 29, - "endPos": { - "offset": 36, - "line": 5, - "column": 7 - }, - "fullEnd": 38, - "start": 33, - "end": 36, - "variable": { - "kind": "", - "startPos": { - "offset": 33, - "line": 5, - "column": 4 - }, - "endPos": { - "offset": 36, - "line": 5, - "column": 7 - }, - "value": "1", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 29, - "line": 5, - "column": 0 - }, - "endPos": { - "offset": 30, - "line": 5, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 29, - "end": 30 - }, - { - "kind": "", - "startPos": { - "offset": 30, - "line": 5, - "column": 1 - }, - "endPos": { - "offset": 31, - "line": 5, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 30, - "end": 31 - }, - { - "kind": "", - "startPos": { - "offset": 31, - "line": 5, - "column": 2 - }, - "endPos": { - "offset": 32, - "line": 5, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 31, - "end": 32 - }, - { - "kind": "", - "startPos": { - "offset": 32, - "line": 5, - "column": 3 - }, - "endPos": { - "offset": 33, - "line": 5, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 32, - "end": 33 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 37, - "line": 5, - "column": 8 - }, - "endPos": { - "offset": 38, - "line": 6, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 37, - "end": 38 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 33, - "end": 36 - } - } - }, - "args": [], - "symbol": 4 - }, - "start": 33, - "end": 36, - "name": "CompileError" - }, { "code": 3027, "diagnostic": "An enum field must be an identifier or a quoted identifier", @@ -3525,12 +3157,14 @@ "isInvalid": false, "start": 51, "end": 52 - } - } + }, + "parent": 14 + }, + "parent": 15 }, "start": 51, "end": 52, "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/nested_duplicate_names.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/nested_duplicate_names.out.json index b985b2dbf..ee8c71c00 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/nested_duplicate_names.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/nested_duplicate_names.out.json @@ -1,6 +1,6 @@ { "value": { - "id": 25, + "id": 22, "kind": "", "startPos": { "offset": 0, @@ -18,7 +18,7 @@ "end": 98, "body": [ { - "id": 24, + "id": 21, "kind": "", "startPos": { "offset": 0, @@ -153,11 +153,13 @@ "isInvalid": false, "start": 8, "end": 9 - } - } + }, + "parent": 1 + }, + "parent": 21 }, "body": { - "id": 23, + "id": 20, "kind": "", "startPos": { "offset": 10, @@ -438,8 +440,10 @@ "isInvalid": false, "start": 23, "end": 24 - } - } + }, + "parent": 5 + }, + "parent": 7 }, "body": { "id": 6, @@ -650,13 +654,13 @@ "isInvalid": false, "start": 34, "end": 35 - } + }, + "parent": 7 }, - "parent": 24, - "symbol": 1 + "parent": 20 }, { - "id": 14, + "id": 13, "kind": "", "startPos": { "offset": 43, @@ -822,7 +826,7 @@ "end": 48 }, "name": { - "id": 12, + "id": 11, "kind": "", "startPos": { "offset": 49, @@ -839,7 +843,7 @@ "start": 49, "end": 50, "expression": { - "id": 11, + "id": 10, "kind": "", "startPos": { "offset": 49, @@ -897,11 +901,13 @@ "isInvalid": false, "start": 49, "end": 50 - } - } + }, + "parent": 11 + }, + "parent": 13 }, "body": { - "id": 13, + "id": 12, "kind": "", "startPos": { "offset": 51, @@ -1109,13 +1115,13 @@ "isInvalid": false, "start": 60, "end": 61 - } + }, + "parent": 13 }, - "parent": 24, - "symbol": 2 + "parent": 20 }, { - "id": 21, + "id": 19, "kind": "", "startPos": { "offset": 69, @@ -1281,7 +1287,7 @@ "end": 74 }, "name": { - "id": 19, + "id": 17, "kind": "", "startPos": { "offset": 75, @@ -1298,7 +1304,7 @@ "start": 75, "end": 76, "expression": { - "id": 18, + "id": 16, "kind": "", "startPos": { "offset": 75, @@ -1356,11 +1362,13 @@ "isInvalid": false, "start": 75, "end": 76 - } - } + }, + "parent": 17 + }, + "parent": 19 }, "body": { - "id": 20, + "id": 18, "kind": "", "startPos": { "offset": 77, @@ -1736,10 +1744,10 @@ "isInvalid": false, "start": 94, "end": 95 - } + }, + "parent": 19 }, - "parent": 24, - "symbol": 3 + "parent": 20 } ], "blockCloseBrace": { @@ -1762,9 +1770,10 @@ "isInvalid": false, "start": 97, "end": 98 - } + }, + "parent": 21 }, - "parent": 25 + "parent": 22 } ], "eof": { @@ -1787,24 +1796,6 @@ "isInvalid": false, "start": 98, "end": 98 - }, - "symbol": { - "symbolTable": { - "Table:A": { - "references": [], - "id": 2, - "symbolTable": {}, - "declaration": 14 - }, - "Table:B": { - "references": [], - "id": 3, - "symbolTable": {}, - "declaration": 21 - } - }, - "id": 0, - "references": [] } }, "errors": [ @@ -2032,8 +2023,10 @@ "isInvalid": false, "start": 23, "end": 24 - } - } + }, + "parent": 5 + }, + "parent": 7 }, "body": { "id": 6, @@ -2244,10 +2237,10 @@ "isInvalid": false, "start": 34, "end": 35 - } + }, + "parent": 7 }, - "parent": 24, - "symbol": 1 + "parent": 20 }, "start": 17, "end": 35, @@ -2257,7 +2250,7 @@ "code": 3010, "diagnostic": "Table must appear top-level", "nodeOrToken": { - "id": 14, + "id": 13, "kind": "", "startPos": { "offset": 43, @@ -2423,7 +2416,7 @@ "end": 48 }, "name": { - "id": 12, + "id": 11, "kind": "", "startPos": { "offset": 49, @@ -2440,7 +2433,7 @@ "start": 49, "end": 50, "expression": { - "id": 11, + "id": 10, "kind": "", "startPos": { "offset": 49, @@ -2498,11 +2491,13 @@ "isInvalid": false, "start": 49, "end": 50 - } - } + }, + "parent": 11 + }, + "parent": 13 }, "body": { - "id": 13, + "id": 12, "kind": "", "startPos": { "offset": 51, @@ -2710,106 +2705,20 @@ "isInvalid": false, "start": 60, "end": 61 - } + }, + "parent": 13 }, - "parent": 24, - "symbol": 2 + "parent": 20 }, "start": 43, "end": 61, "name": "CompileError" }, - { - "code": 3003, - "diagnostic": "Table name 'A' already exists in schema 'public'", - "nodeOrToken": { - "id": 12, - "kind": "", - "startPos": { - "offset": 49, - "line": 5, - "column": 10 - }, - "fullStart": 49, - "endPos": { - "offset": 50, - "line": 5, - "column": 11 - }, - "fullEnd": 51, - "start": 49, - "end": 50, - "expression": { - "id": 11, - "kind": "", - "startPos": { - "offset": 49, - "line": 5, - "column": 10 - }, - "fullStart": 49, - "endPos": { - "offset": 50, - "line": 5, - "column": 11 - }, - "fullEnd": 51, - "start": 49, - "end": 50, - "variable": { - "kind": "", - "startPos": { - "offset": 49, - "line": 5, - "column": 10 - }, - "endPos": { - "offset": 50, - "line": 5, - "column": 11 - }, - "value": "A", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 50, - "line": 5, - "column": 11 - }, - "endPos": { - "offset": 51, - "line": 5, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 50, - "end": 51 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 49, - "end": 50 - } - } - }, - "start": 49, - "end": 50, - "name": "CompileError" - }, { "code": 3010, "diagnostic": "Table must appear top-level", "nodeOrToken": { - "id": 21, + "id": 19, "kind": "", "startPos": { "offset": 69, @@ -2975,7 +2884,7 @@ "end": 74 }, "name": { - "id": 19, + "id": 17, "kind": "", "startPos": { "offset": 75, @@ -2992,7 +2901,7 @@ "start": 75, "end": 76, "expression": { - "id": 18, + "id": 16, "kind": "", "startPos": { "offset": 75, @@ -3050,11 +2959,13 @@ "isInvalid": false, "start": 75, "end": 76 - } - } + }, + "parent": 17 + }, + "parent": 19 }, "body": { - "id": 20, + "id": 18, "kind": "", "startPos": { "offset": 77, @@ -3430,14 +3341,14 @@ "isInvalid": false, "start": 94, "end": 95 - } + }, + "parent": 19 }, - "parent": 24, - "symbol": 3 + "parent": 20 }, "start": 69, "end": 95, "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/public_schema.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/public_schema.out.json index 2b2fc7e5d..1a06d0788 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/public_schema.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/public_schema.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 5, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 3 }, "body": { "id": 2, @@ -281,10 +283,10 @@ "isInvalid": false, "start": 17, "end": 18 - } + }, + "parent": 3 }, - "parent": 11, - "symbol": 1 + "parent": 11 }, { "id": 10, @@ -460,8 +462,10 @@ "isInvalid": false, "start": 27, "end": 33 - } - } + }, + "parent": 5 + }, + "parent": 8 }, "rightExpression": { "id": 7, @@ -539,9 +543,12 @@ "isInvalid": false, "start": 34, "end": 40 - } - } - } + }, + "parent": 7 + }, + "parent": 8 + }, + "parent": 10 }, "body": { "id": 9, @@ -730,10 +737,10 @@ "isInvalid": false, "start": 50, "end": 51 - } + }, + "parent": 10 }, - "parent": 11, - "symbol": 2 + "parent": 11 } ], "eof": { @@ -756,18 +763,6 @@ "isInvalid": false, "start": 51, "end": 51 - }, - "symbol": { - "symbolTable": { - "Enum:status": { - "references": [], - "id": 2, - "symbolTable": {}, - "declaration": 10 - } - }, - "id": 0, - "references": [] } }, "errors": [ @@ -910,8 +905,10 @@ "isInvalid": false, "start": 5, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 3 }, "body": { "id": 2, @@ -1038,197 +1035,15 @@ "isInvalid": false, "start": 17, "end": 18 - } + }, + "parent": 3 }, - "parent": 11, - "symbol": 1 + "parent": 11 }, "start": 0, "end": 18, "name": "CompileError" }, - { - "code": 3003, - "diagnostic": "Enum name status already exists in schema 'public'", - "nodeOrToken": { - "id": 8, - "kind": "", - "startPos": { - "offset": 27, - "line": 4, - "column": 5 - }, - "fullStart": 27, - "endPos": { - "offset": 40, - "line": 4, - "column": 18 - }, - "fullEnd": 41, - "start": 27, - "end": 40, - "op": { - "kind": "", - "startPos": { - "offset": 33, - "line": 4, - "column": 11 - }, - "endPos": { - "offset": 34, - "line": 4, - "column": 12 - }, - "value": ".", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 33, - "end": 34 - }, - "leftExpression": { - "id": 5, - "kind": "", - "startPos": { - "offset": 27, - "line": 4, - "column": 5 - }, - "fullStart": 27, - "endPos": { - "offset": 33, - "line": 4, - "column": 11 - }, - "fullEnd": 33, - "start": 27, - "end": 33, - "expression": { - "id": 4, - "kind": "", - "startPos": { - "offset": 27, - "line": 4, - "column": 5 - }, - "fullStart": 27, - "endPos": { - "offset": 33, - "line": 4, - "column": 11 - }, - "fullEnd": 33, - "start": 27, - "end": 33, - "variable": { - "kind": "", - "startPos": { - "offset": 27, - "line": 4, - "column": 5 - }, - "endPos": { - "offset": 33, - "line": 4, - "column": 11 - }, - "value": "public", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 27, - "end": 33 - } - } - }, - "rightExpression": { - "id": 7, - "kind": "", - "startPos": { - "offset": 34, - "line": 4, - "column": 12 - }, - "fullStart": 34, - "endPos": { - "offset": 40, - "line": 4, - "column": 18 - }, - "fullEnd": 41, - "start": 34, - "end": 40, - "expression": { - "id": 6, - "kind": "", - "startPos": { - "offset": 34, - "line": 4, - "column": 12 - }, - "fullStart": 34, - "endPos": { - "offset": 40, - "line": 4, - "column": 18 - }, - "fullEnd": 41, - "start": 34, - "end": 40, - "variable": { - "kind": "", - "startPos": { - "offset": 34, - "line": 4, - "column": 12 - }, - "endPos": { - "offset": 40, - "line": 4, - "column": 18 - }, - "value": "status", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 40, - "line": 4, - "column": 18 - }, - "endPos": { - "offset": 41, - "line": 4, - "column": 19 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 40, - "end": 41 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 34, - "end": 40 - } - } - } - }, - "start": 27, - "end": 40, - "name": "CompileError" - }, { "code": 3033, "diagnostic": "An Enum must have at least one element", @@ -1406,8 +1221,10 @@ "isInvalid": false, "start": 27, "end": 33 - } - } + }, + "parent": 5 + }, + "parent": 8 }, "rightExpression": { "id": 7, @@ -1485,9 +1302,12 @@ "isInvalid": false, "start": 34, "end": 40 - } - } - } + }, + "parent": 7 + }, + "parent": 8 + }, + "parent": 10 }, "body": { "id": 9, @@ -1676,14 +1496,14 @@ "isInvalid": false, "start": 50, "end": 51 - } + }, + "parent": 10 }, - "parent": 11, - "symbol": 2 + "parent": 11 }, "start": 22, "end": 51, "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/sticky_notes.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/sticky_notes.out.json index 540c8d0be..f041c6590 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/sticky_notes.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/sticky_notes.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 6, "end": 11 - } - } + }, + "parent": 1 + }, + "parent": 30 }, "attributeList": { "id": 6, @@ -251,7 +253,8 @@ "start": 13, "end": 24 } - ] + ], + "parent": 5 }, "value": { "id": 4, @@ -307,8 +310,10 @@ "isInvalid": false, "start": 26, "end": 33 - } - } + }, + "parent": 4 + }, + "parent": 5 }, "colon": { "kind": "", @@ -352,7 +357,8 @@ "isInvalid": false, "start": 24, "end": 25 - } + }, + "parent": 6 } ], "commaList": [], @@ -398,7 +404,8 @@ "isInvalid": false, "start": 33, "end": 34 - } + }, + "parent": 30 }, "body": { "id": 29, @@ -597,8 +604,10 @@ "isInvalid": false, "start": 39, "end": 41 - } - } + }, + "parent": 8 + }, + "parent": 14 }, "args": [ { @@ -677,8 +686,10 @@ "isInvalid": false, "start": 42, "end": 49 - } - } + }, + "parent": 10 + }, + "parent": 14 }, { "id": 13, @@ -818,8 +829,10 @@ "start": 59, "end": 62 } - ] - } + ], + "parent": 12 + }, + "parent": 13 } ], "commaList": [], @@ -865,10 +878,11 @@ "isInvalid": false, "start": 62, "end": 63 - } + }, + "parent": 14 } ], - "symbol": 2 + "parent": 29 }, { "id": 28, @@ -1006,8 +1020,10 @@ "isInvalid": false, "start": 66, "end": 74 - } - } + }, + "parent": 16 + }, + "parent": 28 }, "args": [ { @@ -1081,8 +1097,10 @@ "isInvalid": false, "start": 75, "end": 82 - } - } + }, + "parent": 18 + }, + "parent": 22 }, "argumentList": { "id": 21, @@ -1177,8 +1195,10 @@ "isInvalid": false, "start": 83, "end": 86 - } - } + }, + "parent": 20 + }, + "parent": 21 } ], "commaList": [], @@ -1224,8 +1244,10 @@ "isInvalid": false, "start": 86, "end": 87 - } - } + }, + "parent": 22 + }, + "parent": 28 }, { "id": 27, @@ -1365,8 +1387,10 @@ "start": 93, "end": 97 } - ] - } + ], + "parent": 24 + }, + "parent": 27 }, { "id": 26, @@ -1424,8 +1448,10 @@ "start": 99, "end": 105 } - ] - } + ], + "parent": 26 + }, + "parent": 27 } ], "commaList": [ @@ -1515,10 +1541,11 @@ "isInvalid": false, "start": 105, "end": 106 - } + }, + "parent": 28 } ], - "symbol": 3 + "parent": 29 } ], "blockCloseBrace": { @@ -1563,10 +1590,10 @@ "isInvalid": false, "start": 107, "end": 108 - } + }, + "parent": 30 }, - "parent": 93, - "symbol": 1 + "parent": 93 }, { "id": 37, @@ -1726,8 +1753,10 @@ "isInvalid": false, "start": 115, "end": 120 - } - } + }, + "parent": 32 + }, + "parent": 37 }, "body": { "id": 36, @@ -1926,10 +1955,13 @@ "isInvalid": false, "start": 125, "end": 140 - } - } + }, + "parent": 34 + }, + "parent": 35 }, - "args": [] + "args": [], + "parent": 36 } ], "blockCloseBrace": { @@ -1974,7 +2006,8 @@ "isInvalid": false, "start": 141, "end": 142 - } + }, + "parent": 37 }, "parent": 93 }, @@ -2136,8 +2169,10 @@ "isInvalid": false, "start": 149, "end": 154 - } - } + }, + "parent": 39 + }, + "parent": 44 }, "body": { "id": 43, @@ -2336,10 +2371,13 @@ "isInvalid": false, "start": 159, "end": 174 - } - } + }, + "parent": 41 + }, + "parent": 42 }, - "args": [] + "args": [], + "parent": 43 } ], "blockCloseBrace": { @@ -2384,7 +2422,8 @@ "isInvalid": false, "start": 175, "end": 176 - } + }, + "parent": 44 }, "parent": 93 }, @@ -2546,8 +2585,10 @@ "isInvalid": false, "start": 183, "end": 188 - } - } + }, + "parent": 46 + }, + "parent": 51 }, "body": { "id": 50, @@ -2746,10 +2787,13 @@ "isInvalid": false, "start": 193, "end": 208 - } - } + }, + "parent": 48 + }, + "parent": 49 }, - "args": [] + "args": [], + "parent": 50 } ], "blockCloseBrace": { @@ -2794,7 +2838,8 @@ "isInvalid": false, "start": 209, "end": 210 - } + }, + "parent": 51 }, "parent": 93 }, @@ -2956,8 +3001,10 @@ "isInvalid": false, "start": 217, "end": 224 - } - } + }, + "parent": 53 + }, + "parent": 58 }, "body": { "id": 57, @@ -3156,10 +3203,13 @@ "isInvalid": false, "start": 229, "end": 244 - } - } + }, + "parent": 55 + }, + "parent": 56 }, - "args": [] + "args": [], + "parent": 57 } ], "blockCloseBrace": { @@ -3204,7 +3254,8 @@ "isInvalid": false, "start": 245, "end": 246 - } + }, + "parent": 58 }, "parent": 93 }, @@ -3399,8 +3450,10 @@ "isInvalid": false, "start": 253, "end": 259 - } - } + }, + "parent": 60 + }, + "parent": 63 }, "rightExpression": { "id": 62, @@ -3456,9 +3509,12 @@ "isInvalid": false, "start": 260, "end": 265 - } - } - } + }, + "parent": 62 + }, + "parent": 63 + }, + "parent": 65 }, "indexer": { "id": 64, @@ -3542,8 +3598,10 @@ "isInvalid": false, "start": 266, "end": 267 - } - } + }, + "parent": 65 + }, + "parent": 70 }, "body": { "id": 69, @@ -3742,10 +3800,13 @@ "isInvalid": false, "start": 272, "end": 302 - } - } + }, + "parent": 67 + }, + "parent": 68 }, - "args": [] + "args": [], + "parent": 69 } ], "blockCloseBrace": { @@ -3790,7 +3851,8 @@ "isInvalid": false, "start": 303, "end": 304 - } + }, + "parent": 70 }, "parent": 93 }, @@ -3930,8 +3992,10 @@ "isInvalid": false, "start": 310, "end": 324 - } - } + }, + "parent": 72 + }, + "parent": 77 }, "body": { "id": 76, @@ -4130,10 +4194,13 @@ "isInvalid": false, "start": 329, "end": 359 - } - } + }, + "parent": 74 + }, + "parent": 75 }, - "args": [] + "args": [], + "parent": 76 } ], "blockCloseBrace": { @@ -4178,7 +4245,8 @@ "isInvalid": false, "start": 360, "end": 361 - } + }, + "parent": 77 }, "parent": 93 }, @@ -4356,8 +4424,10 @@ "isInvalid": false, "start": 368, "end": 376 - } - } + }, + "parent": 79 + }, + "parent": 82 }, "rightExpression": { "id": 81, @@ -4435,9 +4505,12 @@ "isInvalid": false, "start": 377, "end": 384 - } - } - } + }, + "parent": 81 + }, + "parent": 82 + }, + "parent": 92 }, "attributeList": { "id": 87, @@ -4534,7 +4607,8 @@ "start": 386, "end": 397 } - ] + ], + "parent": 86 }, "value": { "id": 85, @@ -4590,8 +4664,10 @@ "isInvalid": false, "start": 399, "end": 406 - } - } + }, + "parent": 85 + }, + "parent": 86 }, "colon": { "kind": "", @@ -4635,7 +4711,8 @@ "isInvalid": false, "start": 397, "end": 398 - } + }, + "parent": 87 } ], "commaList": [], @@ -4681,7 +4758,8 @@ "isInvalid": false, "start": 406, "end": 407 - } + }, + "parent": 92 }, "body": { "id": 91, @@ -4880,10 +4958,13 @@ "isInvalid": false, "start": 412, "end": 442 - } - } + }, + "parent": 89 + }, + "parent": 90 }, - "args": [] + "args": [], + "parent": 91 } ], "blockCloseBrace": { @@ -4928,7 +5009,8 @@ "isInvalid": false, "start": 443, "end": 444 - } + }, + "parent": 92 }, "parent": 93 } @@ -4953,204 +5035,9 @@ "isInvalid": false, "start": 445, "end": 445 - }, - "symbol": { - "symbolTable": { - "Table:users": { - "references": [], - "id": 1, - "symbolTable": { - "Column:id": { - "references": [], - "id": 2, - "declaration": 14 - }, - "Column:username": { - "references": [], - "id": 3, - "declaration": 28 - } - }, - "declaration": 30 - } - }, - "id": 0, - "references": [] } }, "errors": [ - { - "code": 3003, - "diagnostic": "Sticky note \"note2\" has already been defined", - "nodeOrToken": { - "id": 39, - "kind": "", - "startPos": { - "offset": 149, - "line": 9, - "column": 5 - }, - "fullStart": 149, - "endPos": { - "offset": 154, - "line": 9, - "column": 10 - }, - "fullEnd": 155, - "start": 149, - "end": 154, - "expression": { - "id": 38, - "kind": "", - "startPos": { - "offset": 149, - "line": 9, - "column": 5 - }, - "fullStart": 149, - "endPos": { - "offset": 154, - "line": 9, - "column": 10 - }, - "fullEnd": 155, - "start": 149, - "end": 154, - "variable": { - "kind": "", - "startPos": { - "offset": 149, - "line": 9, - "column": 5 - }, - "endPos": { - "offset": 154, - "line": 9, - "column": 10 - }, - "value": "note2", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 154, - "line": 9, - "column": 10 - }, - "endPos": { - "offset": 155, - "line": 9, - "column": 11 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 154, - "end": 155 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 149, - "end": 154 - } - } - }, - "start": 149, - "end": 154, - "name": "CompileError" - }, - { - "code": 3003, - "diagnostic": "Sticky note \"note3\" has already been defined", - "nodeOrToken": { - "id": 53, - "kind": "", - "startPos": { - "offset": 217, - "line": 17, - "column": 5 - }, - "fullStart": 217, - "endPos": { - "offset": 224, - "line": 17, - "column": 12 - }, - "fullEnd": 225, - "start": 217, - "end": 224, - "expression": { - "id": 52, - "kind": "", - "startPos": { - "offset": 217, - "line": 17, - "column": 5 - }, - "fullStart": 217, - "endPos": { - "offset": 224, - "line": 17, - "column": 12 - }, - "fullEnd": 225, - "start": 217, - "end": 224, - "variable": { - "kind": "", - "startPos": { - "offset": 217, - "line": 17, - "column": 5 - }, - "endPos": { - "offset": 224, - "line": 17, - "column": 12 - }, - "value": "note3", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 224, - "line": 17, - "column": 12 - }, - "endPos": { - "offset": 225, - "line": 17, - "column": 13 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 224, - "end": 225 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 217, - "end": 224 - } - } - }, - "start": 217, - "end": 224, - "name": "CompileError" - }, { "code": 3000, "diagnostic": "Invalid name for sticky note ", @@ -5345,8 +5232,10 @@ "isInvalid": false, "start": 253, "end": 259 - } - } + }, + "parent": 60 + }, + "parent": 63 }, "rightExpression": { "id": 62, @@ -5402,9 +5291,12 @@ "isInvalid": false, "start": 260, "end": 265 - } - } - } + }, + "parent": 62 + }, + "parent": 63 + }, + "parent": 65 }, "indexer": { "id": 64, @@ -5488,8 +5380,10 @@ "isInvalid": false, "start": 266, "end": 267 - } - } + }, + "parent": 65 + }, + "parent": 70 }, "body": { "id": 69, @@ -5688,10 +5582,13 @@ "isInvalid": false, "start": 272, "end": 302 - } - } + }, + "parent": 67 + }, + "parent": 68 }, - "args": [] + "args": [], + "parent": 69 } ], "blockCloseBrace": { @@ -5736,7 +5633,8 @@ "isInvalid": false, "start": 303, "end": 304 - } + }, + "parent": 70 }, "parent": 93 }, @@ -5842,7 +5740,8 @@ "start": 386, "end": 397 } - ] + ], + "parent": 86 }, "value": { "id": 85, @@ -5898,8 +5797,10 @@ "isInvalid": false, "start": 399, "end": 406 - } - } + }, + "parent": 85 + }, + "parent": 86 }, "colon": { "kind": "", @@ -5943,7 +5844,8 @@ "isInvalid": false, "start": 397, "end": 398 - } + }, + "parent": 87 } ], "commaList": [], @@ -5989,11 +5891,12 @@ "isInvalid": false, "start": 406, "end": 407 - } + }, + "parent": 92 }, "start": 385, "end": 407, "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/output/wrong_sub_element_declarations.out.json b/packages/dbml-parse/__tests__/snapshots/validator/output/wrong_sub_element_declarations.out.json index 2e5380b9c..f380ab4d5 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/output/wrong_sub_element_declarations.out.json +++ b/packages/dbml-parse/__tests__/snapshots/validator/output/wrong_sub_element_declarations.out.json @@ -153,8 +153,10 @@ "isInvalid": false, "start": 6, "end": 7 - } - } + }, + "parent": 1 + }, + "parent": 33 }, "body": { "id": 32, @@ -395,8 +397,10 @@ "isInvalid": false, "start": 14, "end": 21 - } - } + }, + "parent": 3 + }, + "parent": 11 }, "args": [ { @@ -475,8 +479,10 @@ "isInvalid": false, "start": 22, "end": 27 - } - } + }, + "parent": 5 + }, + "parent": 11 }, { "id": 7, @@ -554,8 +560,10 @@ "isInvalid": false, "start": 28, "end": 33 - } - } + }, + "parent": 7 + }, + "parent": 11 }, { "id": 9, @@ -633,8 +641,10 @@ "isInvalid": false, "start": 34, "end": 39 - } - } + }, + "parent": 9 + }, + "parent": 11 }, { "id": 10, @@ -759,10 +769,11 @@ "isInvalid": false, "start": 41, "end": 42 - } + }, + "parent": 11 } ], - "symbol": 2 + "parent": 32 }, { "id": 19, @@ -942,8 +953,10 @@ "isInvalid": false, "start": 90, "end": 97 - } - } + }, + "parent": 13 + }, + "parent": 19 }, "args": [ { @@ -1022,8 +1035,10 @@ "isInvalid": false, "start": 98, "end": 99 - } - } + }, + "parent": 15 + }, + "parent": 19 }, { "id": 17, @@ -1101,8 +1116,10 @@ "isInvalid": false, "start": 100, "end": 104 - } - } + }, + "parent": 17 + }, + "parent": 19 }, { "id": 18, @@ -1227,10 +1244,11 @@ "isInvalid": false, "start": 106, "end": 107 - } + }, + "parent": 19 } ], - "symbol": 3 + "parent": 32 }, { "id": 31, @@ -1410,8 +1428,10 @@ "isInvalid": false, "start": 146, "end": 153 - } - } + }, + "parent": 21 + }, + "parent": 31 }, "args": [ { @@ -1490,8 +1510,10 @@ "isInvalid": false, "start": 154, "end": 159 - } - } + }, + "parent": 23 + }, + "parent": 31 }, { "id": 25, @@ -1569,8 +1591,10 @@ "isInvalid": false, "start": 160, "end": 166 - } - } + }, + "parent": 25 + }, + "parent": 31 }, { "id": 27, @@ -1648,8 +1672,10 @@ "isInvalid": false, "start": 167, "end": 169 - } - } + }, + "parent": 27 + }, + "parent": 31 }, { "id": 29, @@ -1727,8 +1753,10 @@ "isInvalid": false, "start": 170, "end": 174 - } - } + }, + "parent": 29 + }, + "parent": 31 }, { "id": 30, @@ -1853,10 +1881,11 @@ "isInvalid": false, "start": 176, "end": 177 - } + }, + "parent": 31 } ], - "symbol": 4 + "parent": 32 } ], "blockCloseBrace": { @@ -1901,10 +1930,10 @@ "isInvalid": false, "start": 212, "end": 213 - } + }, + "parent": 33 }, - "parent": 34, - "symbol": 1 + "parent": 34 } ], "eof": { @@ -1927,24 +1956,6 @@ "isInvalid": false, "start": 214, "end": 214 - }, - "symbol": { - "symbolTable": { - "Table:A": { - "references": [], - "id": 1, - "symbolTable": { - "Column:Indexes": { - "references": [], - "id": 2, - "declaration": 11 - } - }, - "declaration": 33 - } - }, - "id": 0, - "references": [] } }, "errors": [ @@ -2027,8 +2038,10 @@ "isInvalid": false, "start": 28, "end": 33 - } - } + }, + "parent": 7 + }, + "parent": 11 }, "start": 28, "end": 33, @@ -2113,8 +2126,10 @@ "isInvalid": false, "start": 34, "end": 39 - } - } + }, + "parent": 9 + }, + "parent": 11 }, "start": 34, "end": 39, @@ -2246,7 +2261,8 @@ "isInvalid": false, "start": 41, "end": 42 - } + }, + "parent": 11 }, "start": 40, "end": 42, @@ -2331,8 +2347,10 @@ "isInvalid": false, "start": 98, "end": 99 - } - } + }, + "parent": 15 + }, + "parent": 19 }, "start": 98, "end": 99, @@ -2417,8 +2435,10 @@ "isInvalid": false, "start": 100, "end": 104 - } - } + }, + "parent": 17 + }, + "parent": 19 }, "start": 100, "end": 104, @@ -2550,2617 +2570,409 @@ "isInvalid": false, "start": 106, "end": 107 - } + }, + "parent": 19 }, "start": 105, "end": 107, "name": "CompileError" }, { - "code": 3023, - "diagnostic": "Duplicate column Indexes", + "code": 3019, + "diagnostic": "These fields must be some inline settings optionally ended with a setting list", "nodeOrToken": { - "id": 19, - "kind": "", + "id": 25, + "kind": "", "startPos": { - "offset": 90, - "line": 2, - "column": 4 + "offset": 160, + "line": 3, + "column": 18 }, - "fullStart": 86, + "fullStart": 160, "endPos": { - "offset": 107, - "line": 2, - "column": 21 + "offset": 166, + "line": 3, + "column": 24 }, - "fullEnd": 142, - "start": 90, - "end": 107, - "callee": { - "id": 13, - "kind": "", + "fullEnd": 167, + "start": 160, + "end": 166, + "expression": { + "id": 24, + "kind": "", "startPos": { - "offset": 90, - "line": 2, - "column": 4 + "offset": 160, + "line": 3, + "column": 18 }, - "fullStart": 86, + "fullStart": 160, "endPos": { - "offset": 97, - "line": 2, - "column": 11 + "offset": 166, + "line": 3, + "column": 24 }, - "fullEnd": 98, - "start": 90, - "end": 97, - "expression": { - "id": 12, - "kind": "", - "startPos": { - "offset": 90, - "line": 2, - "column": 4 - }, - "fullStart": 86, - "endPos": { - "offset": 97, - "line": 2, - "column": 11 - }, - "fullEnd": 98, - "start": 90, - "end": 97, - "variable": { - "kind": "", - "startPos": { - "offset": 90, - "line": 2, - "column": 4 - }, - "endPos": { - "offset": 97, - "line": 2, - "column": 11 - }, - "value": "Indexes", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 86, - "line": 2, - "column": 0 - }, - "endPos": { - "offset": 87, - "line": 2, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 86, - "end": 87 - }, - { - "kind": "", - "startPos": { - "offset": 87, - "line": 2, - "column": 1 - }, - "endPos": { - "offset": 88, - "line": 2, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 87, - "end": 88 - }, - { - "kind": "", - "startPos": { - "offset": 88, - "line": 2, - "column": 2 - }, - "endPos": { - "offset": 89, - "line": 2, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 88, - "end": 89 - }, - { - "kind": "", - "startPos": { - "offset": 89, - "line": 2, - "column": 3 - }, - "endPos": { - "offset": 90, - "line": 2, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 89, - "end": 90 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 97, - "line": 2, - "column": 11 - }, - "endPos": { - "offset": 98, - "line": 2, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 97, - "end": 98 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 90, - "end": 97 - } - } - }, - "args": [ - { - "id": 15, - "kind": "", + "fullEnd": 167, + "start": 160, + "end": 166, + "variable": { + "kind": "", "startPos": { - "offset": 98, - "line": 2, - "column": 12 + "offset": 160, + "line": 3, + "column": 18 }, - "fullStart": 98, "endPos": { - "offset": 99, - "line": 2, - "column": 13 + "offset": 166, + "line": 3, + "column": 24 }, - "fullEnd": 100, - "start": 98, - "end": 99, - "expression": { - "id": 14, - "kind": "", - "startPos": { - "offset": 98, - "line": 2, - "column": 12 - }, - "fullStart": 98, - "endPos": { - "offset": 99, - "line": 2, - "column": 13 - }, - "fullEnd": 100, - "start": 98, - "end": 99, - "literal": { - "kind": "", + "value": "number", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", "startPos": { - "offset": 98, - "line": 2, - "column": 12 + "offset": 166, + "line": 3, + "column": 24 }, "endPos": { - "offset": 99, - "line": 2, - "column": 13 + "offset": 167, + "line": 3, + "column": 25 }, - "value": "2", + "value": " ", "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 99, - "line": 2, - "column": 13 - }, - "endPos": { - "offset": 100, - "line": 2, - "column": 14 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 99, - "end": 100 - } - ], + "trailingTrivia": [], "leadingInvalid": [], "trailingInvalid": [], "isInvalid": false, - "start": 98, - "end": 99 + "start": 166, + "end": 167 } - } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 160, + "end": 166 }, - { - "id": 17, - "kind": "", - "startPos": { - "offset": 100, - "line": 2, - "column": 14 + "parent": 25 + }, + "parent": 31 + }, + "start": 160, + "end": 166, + "name": "CompileError" + }, + { + "code": 3019, + "diagnostic": "These fields must be some inline settings optionally ended with a setting list", + "nodeOrToken": { + "id": 27, + "kind": "", + "startPos": { + "offset": 167, + "line": 3, + "column": 25 + }, + "fullStart": 167, + "endPos": { + "offset": 169, + "line": 3, + "column": 27 + }, + "fullEnd": 170, + "start": 167, + "end": 169, + "expression": { + "id": 26, + "kind": "", + "startPos": { + "offset": 167, + "line": 3, + "column": 25 + }, + "fullStart": 167, + "endPos": { + "offset": 169, + "line": 3, + "column": 27 + }, + "fullEnd": 170, + "start": 167, + "end": 169, + "variable": { + "kind": "", + "startPos": { + "offset": 167, + "line": 3, + "column": 25 }, - "fullStart": 100, "endPos": { - "offset": 104, - "line": 2, - "column": 18 + "offset": 169, + "line": 3, + "column": 27 }, - "fullEnd": 105, - "start": 100, - "end": 104, - "expression": { - "id": 16, - "kind": "", - "startPos": { - "offset": 100, - "line": 2, - "column": 14 - }, - "fullStart": 100, - "endPos": { - "offset": 104, - "line": 2, - "column": 18 - }, - "fullEnd": 105, - "start": 100, - "end": 104, - "variable": { - "kind": "", + "value": "of", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", "startPos": { - "offset": 100, - "line": 2, - "column": 14 + "offset": 169, + "line": 3, + "column": 27 }, "endPos": { - "offset": 104, - "line": 2, - "column": 18 + "offset": 170, + "line": 3, + "column": 28 }, - "value": "args", + "value": " ", "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 104, - "line": 2, - "column": 18 - }, - "endPos": { - "offset": 105, - "line": 2, - "column": 19 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 104, - "end": 105 - } - ], + "trailingTrivia": [], "leadingInvalid": [], "trailingInvalid": [], "isInvalid": false, - "start": 100, - "end": 104 + "start": 169, + "end": 170 } - } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 167, + "end": 169 }, - { - "id": 18, - "kind": "", - "startPos": { - "offset": 105, - "line": 2, - "column": 19 - }, - "fullStart": 105, - "endPos": { - "offset": 107, - "line": 2, - "column": 21 - }, - "fullEnd": 142, - "start": 105, - "end": 107, - "blockOpenBrace": { - "kind": "", - "startPos": { - "offset": 105, - "line": 2, - "column": 19 - }, - "endPos": { - "offset": 106, - "line": 2, - "column": 20 - }, - "value": "{", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 105, - "end": 106 - }, - "body": [], - "blockCloseBrace": { - "kind": "", - "startPos": { - "offset": 106, - "line": 2, - "column": 20 - }, - "endPos": { - "offset": 107, - "line": 2, - "column": 21 - }, - "value": "}", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 107, - "line": 2, - "column": 21 - }, - "endPos": { - "offset": 108, - "line": 2, - "column": 22 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 107, - "end": 108 - }, - { - "kind": "", - "startPos": { - "offset": 108, - "line": 2, - "column": 22 - }, - "endPos": { - "offset": 141, - "line": 2, - "column": 55 - }, - "value": " this is also treated as column", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 108, - "end": 141 - }, - { - "kind": "", - "startPos": { - "offset": 141, - "line": 2, - "column": 55 - }, - "endPos": { - "offset": 142, - "line": 3, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 141, - "end": 142 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 106, - "end": 107 - } - } - ], - "symbol": 3 + "parent": 27 + }, + "parent": 31 }, - "start": 90, - "end": 107, + "start": 167, + "end": 169, "name": "CompileError" }, { - "code": 3023, - "diagnostic": "Duplicate column Indexes", + "code": 3019, + "diagnostic": "These fields must be some inline settings optionally ended with a setting list", "nodeOrToken": { - "id": 11, - "kind": "", + "id": 29, + "kind": "", "startPos": { - "offset": 14, - "line": 1, - "column": 4 + "offset": 170, + "line": 3, + "column": 28 }, - "fullStart": 10, + "fullStart": 170, "endPos": { - "offset": 42, - "line": 1, + "offset": 174, + "line": 3, "column": 32 }, - "fullEnd": 86, - "start": 14, - "end": 42, - "callee": { - "id": 3, - "kind": "", - "startPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "fullStart": 10, - "endPos": { - "offset": 21, - "line": 1, - "column": 11 - }, - "fullEnd": 22, - "start": 14, - "end": 21, - "expression": { - "id": 2, - "kind": "", - "startPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "fullStart": 10, - "endPos": { - "offset": 21, - "line": 1, - "column": 11 - }, - "fullEnd": 22, - "start": 14, - "end": 21, - "variable": { - "kind": "", - "startPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "endPos": { - "offset": 21, - "line": 1, - "column": 11 - }, - "value": "Indexes", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 10, - "line": 1, - "column": 0 - }, - "endPos": { - "offset": 11, - "line": 1, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 10, - "end": 11 - }, - { - "kind": "", - "startPos": { - "offset": 11, - "line": 1, - "column": 1 - }, - "endPos": { - "offset": 12, - "line": 1, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 11, - "end": 12 - }, - { - "kind": "", - "startPos": { - "offset": 12, - "line": 1, - "column": 2 - }, - "endPos": { - "offset": 13, - "line": 1, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 12, - "end": 13 - }, - { - "kind": "", - "startPos": { - "offset": 13, - "line": 1, - "column": 3 - }, - "endPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 13, - "end": 14 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 21, - "line": 1, - "column": 11 - }, - "endPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 21, - "end": 22 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 14, - "end": 21 - } - } - }, - "args": [ - { - "id": 5, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "fullStart": 22, - "endPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "fullEnd": 28, - "start": 22, - "end": 27, - "expression": { - "id": 4, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "fullStart": 22, - "endPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "fullEnd": 28, - "start": 22, - "end": 27, - "variable": { - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "endPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "value": "wrong", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "endPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 27, - "end": 28 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 22, - "end": 27 - } - } - }, - { - "id": 7, - "kind": "", - "startPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "fullStart": 28, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 34, - "start": 28, - "end": 33, - "expression": { - "id": 6, - "kind": "", - "startPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "fullStart": 28, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 34, - "start": 28, - "end": 33, - "variable": { - "kind": "", - "startPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "value": "index", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "endPos": { - "offset": 34, - "line": 1, - "column": 24 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 33, - "end": 34 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 28, - "end": 33 - } - } - }, - { - "id": 9, - "kind": "", - "startPos": { - "offset": 34, - "line": 1, - "column": 24 - }, - "fullStart": 34, - "endPos": { - "offset": 39, - "line": 1, - "column": 29 - }, - "fullEnd": 40, - "start": 34, - "end": 39, - "expression": { - "id": 8, - "kind": "", - "startPos": { - "offset": 34, - "line": 1, - "column": 24 - }, - "fullStart": 34, - "endPos": { - "offset": 39, - "line": 1, - "column": 29 - }, - "fullEnd": 40, - "start": 34, - "end": 39, - "variable": { - "kind": "", - "startPos": { - "offset": 34, - "line": 1, - "column": 24 - }, - "endPos": { - "offset": 39, - "line": 1, - "column": 29 - }, - "value": "alias", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 39, - "line": 1, - "column": 29 - }, - "endPos": { - "offset": 40, - "line": 1, - "column": 30 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 39, - "end": 40 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 34, - "end": 39 - } - } - }, - { - "id": 10, - "kind": "", - "startPos": { - "offset": 40, - "line": 1, - "column": 30 - }, - "fullStart": 40, - "endPos": { - "offset": 42, - "line": 1, - "column": 32 - }, - "fullEnd": 86, - "start": 40, - "end": 42, - "blockOpenBrace": { - "kind": "", - "startPos": { - "offset": 40, - "line": 1, - "column": 30 - }, - "endPos": { - "offset": 41, - "line": 1, - "column": 31 - }, - "value": "{", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 40, - "end": 41 - }, - "body": [], - "blockCloseBrace": { - "kind": "", - "startPos": { - "offset": 41, - "line": 1, - "column": 31 - }, - "endPos": { - "offset": 42, - "line": 1, - "column": 32 - }, - "value": "}", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 42, - "line": 1, - "column": 32 - }, - "endPos": { - "offset": 43, - "line": 1, - "column": 33 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 42, - "end": 43 - }, - { - "kind": "", - "startPos": { - "offset": 43, - "line": 1, - "column": 33 - }, - "endPos": { - "offset": 85, - "line": 1, - "column": 75 - }, - "value": " this indexes is treated as Table column", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 43, - "end": 85 - }, - { - "kind": "", - "startPos": { - "offset": 85, - "line": 1, - "column": 75 - }, - "endPos": { - "offset": 86, - "line": 2, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 85, - "end": 86 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 41, - "end": 42 - } - } - ], - "symbol": 2 - }, - "start": 14, - "end": 42, - "name": "CompileError" - }, - { - "code": 3019, - "diagnostic": "These fields must be some inline settings optionally ended with a setting list", - "nodeOrToken": { - "id": 25, - "kind": "", - "startPos": { - "offset": 160, - "line": 3, - "column": 18 - }, - "fullStart": 160, - "endPos": { - "offset": 166, - "line": 3, - "column": 24 - }, - "fullEnd": 167, - "start": 160, - "end": 166, - "expression": { - "id": 24, - "kind": "", - "startPos": { - "offset": 160, - "line": 3, - "column": 18 - }, - "fullStart": 160, - "endPos": { - "offset": 166, - "line": 3, - "column": 24 - }, - "fullEnd": 167, - "start": 160, - "end": 166, - "variable": { - "kind": "", - "startPos": { - "offset": 160, - "line": 3, - "column": 18 - }, - "endPos": { - "offset": 166, - "line": 3, - "column": 24 - }, - "value": "number", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 166, - "line": 3, - "column": 24 - }, - "endPos": { - "offset": 167, - "line": 3, - "column": 25 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 166, - "end": 167 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 160, - "end": 166 - } - } - }, - "start": 160, - "end": 166, - "name": "CompileError" - }, - { - "code": 3019, - "diagnostic": "These fields must be some inline settings optionally ended with a setting list", - "nodeOrToken": { - "id": 27, - "kind": "", - "startPos": { - "offset": 167, - "line": 3, - "column": 25 - }, - "fullStart": 167, - "endPos": { - "offset": 169, - "line": 3, - "column": 27 - }, - "fullEnd": 170, - "start": 167, - "end": 169, - "expression": { - "id": 26, - "kind": "", - "startPos": { - "offset": 167, - "line": 3, - "column": 25 - }, - "fullStart": 167, - "endPos": { - "offset": 169, - "line": 3, - "column": 27 - }, - "fullEnd": 170, - "start": 167, - "end": 169, - "variable": { - "kind": "", - "startPos": { - "offset": 167, - "line": 3, - "column": 25 - }, - "endPos": { - "offset": 169, - "line": 3, - "column": 27 - }, - "value": "of", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 169, - "line": 3, - "column": 27 - }, - "endPos": { - "offset": 170, - "line": 3, - "column": 28 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 169, - "end": 170 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 167, - "end": 169 - } - } - }, - "start": 167, - "end": 169, - "name": "CompileError" - }, - { - "code": 3019, - "diagnostic": "These fields must be some inline settings optionally ended with a setting list", - "nodeOrToken": { - "id": 29, - "kind": "", - "startPos": { - "offset": 170, - "line": 3, - "column": 28 - }, - "fullStart": 170, - "endPos": { - "offset": 174, - "line": 3, - "column": 32 - }, - "fullEnd": 175, - "start": 170, - "end": 174, - "expression": { - "id": 28, - "kind": "", - "startPos": { - "offset": 170, - "line": 3, - "column": 28 - }, - "fullStart": 170, - "endPos": { - "offset": 174, - "line": 3, - "column": 32 - }, - "fullEnd": 175, - "start": 170, - "end": 174, - "variable": { - "kind": "", - "startPos": { - "offset": 170, - "line": 3, - "column": 28 - }, - "endPos": { - "offset": 174, - "line": 3, - "column": 32 - }, - "value": "args", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 174, - "line": 3, - "column": 32 - }, - "endPos": { - "offset": 175, - "line": 3, - "column": 33 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 174, - "end": 175 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 170, - "end": 174 - } - } - }, - "start": 170, - "end": 174, - "name": "CompileError" - }, - { - "code": 3019, - "diagnostic": "These fields must be some inline settings optionally ended with a setting list", - "nodeOrToken": { - "id": 30, - "kind": "", - "startPos": { - "offset": 175, - "line": 3, - "column": 33 - }, - "fullStart": 175, - "endPos": { - "offset": 177, - "line": 3, - "column": 35 - }, - "fullEnd": 212, - "start": 175, - "end": 177, - "blockOpenBrace": { - "kind": "", - "startPos": { - "offset": 175, - "line": 3, - "column": 33 - }, - "endPos": { - "offset": 176, - "line": 3, - "column": 34 - }, - "value": "{", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 175, - "end": 176 - }, - "body": [], - "blockCloseBrace": { - "kind": "", - "startPos": { - "offset": 176, - "line": 3, - "column": 34 - }, - "endPos": { - "offset": 177, - "line": 3, - "column": 35 - }, - "value": "}", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 177, - "line": 3, - "column": 35 - }, - "endPos": { - "offset": 178, - "line": 3, - "column": 36 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 177, - "end": 178 - }, - { - "kind": "", - "startPos": { - "offset": 178, - "line": 3, - "column": 36 - }, - "endPos": { - "offset": 211, - "line": 3, - "column": 69 - }, - "value": " this is also treated as column", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 178, - "end": 211 - }, - { - "kind": "", - "startPos": { - "offset": 211, - "line": 3, - "column": 69 - }, - "endPos": { - "offset": 212, - "line": 4, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 211, - "end": 212 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 176, - "end": 177 - } - }, - "start": 175, - "end": 177, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column Indexes", - "nodeOrToken": { - "id": 31, - "kind": "", - "startPos": { - "offset": 146, - "line": 3, - "column": 4 - }, - "fullStart": 142, - "endPos": { - "offset": 177, - "line": 3, - "column": 35 - }, - "fullEnd": 212, - "start": 146, - "end": 177, - "callee": { - "id": 21, - "kind": "", - "startPos": { - "offset": 146, - "line": 3, - "column": 4 - }, - "fullStart": 142, - "endPos": { - "offset": 153, - "line": 3, - "column": 11 - }, - "fullEnd": 154, - "start": 146, - "end": 153, - "expression": { - "id": 20, - "kind": "", - "startPos": { - "offset": 146, - "line": 3, - "column": 4 - }, - "fullStart": 142, - "endPos": { - "offset": 153, - "line": 3, - "column": 11 - }, - "fullEnd": 154, - "start": 146, - "end": 153, - "variable": { - "kind": "", - "startPos": { - "offset": 146, - "line": 3, - "column": 4 - }, - "endPos": { - "offset": 153, - "line": 3, - "column": 11 - }, - "value": "Indexes", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 142, - "line": 3, - "column": 0 - }, - "endPos": { - "offset": 143, - "line": 3, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 142, - "end": 143 - }, - { - "kind": "", - "startPos": { - "offset": 143, - "line": 3, - "column": 1 - }, - "endPos": { - "offset": 144, - "line": 3, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 143, - "end": 144 - }, - { - "kind": "", - "startPos": { - "offset": 144, - "line": 3, - "column": 2 - }, - "endPos": { - "offset": 145, - "line": 3, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 144, - "end": 145 - }, - { - "kind": "", - "startPos": { - "offset": 145, - "line": 3, - "column": 3 - }, - "endPos": { - "offset": 146, - "line": 3, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 145, - "end": 146 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 153, - "line": 3, - "column": 11 - }, - "endPos": { - "offset": 154, - "line": 3, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 153, - "end": 154 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 146, - "end": 153 - } - } - }, - "args": [ - { - "id": 23, - "kind": "", - "startPos": { - "offset": 154, - "line": 3, - "column": 12 - }, - "fullStart": 154, - "endPos": { - "offset": 159, - "line": 3, - "column": 17 - }, - "fullEnd": 160, - "start": 154, - "end": 159, - "expression": { - "id": 22, - "kind": "", - "startPos": { - "offset": 154, - "line": 3, - "column": 12 - }, - "fullStart": 154, - "endPos": { - "offset": 159, - "line": 3, - "column": 17 - }, - "fullEnd": 160, - "start": 154, - "end": 159, - "variable": { - "kind": "", - "startPos": { - "offset": 154, - "line": 3, - "column": 12 - }, - "endPos": { - "offset": 159, - "line": 3, - "column": 17 - }, - "value": "wrong", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 159, - "line": 3, - "column": 17 - }, - "endPos": { - "offset": 160, - "line": 3, - "column": 18 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 159, - "end": 160 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 154, - "end": 159 - } - } - }, - { - "id": 25, - "kind": "", - "startPos": { - "offset": 160, - "line": 3, - "column": 18 - }, - "fullStart": 160, - "endPos": { - "offset": 166, - "line": 3, - "column": 24 - }, - "fullEnd": 167, - "start": 160, - "end": 166, - "expression": { - "id": 24, - "kind": "", - "startPos": { - "offset": 160, - "line": 3, - "column": 18 - }, - "fullStart": 160, - "endPos": { - "offset": 166, - "line": 3, - "column": 24 - }, - "fullEnd": 167, - "start": 160, - "end": 166, - "variable": { - "kind": "", - "startPos": { - "offset": 160, - "line": 3, - "column": 18 - }, - "endPos": { - "offset": 166, - "line": 3, - "column": 24 - }, - "value": "number", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 166, - "line": 3, - "column": 24 - }, - "endPos": { - "offset": 167, - "line": 3, - "column": 25 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 166, - "end": 167 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 160, - "end": 166 - } - } - }, - { - "id": 27, - "kind": "", - "startPos": { - "offset": 167, - "line": 3, - "column": 25 - }, - "fullStart": 167, - "endPos": { - "offset": 169, - "line": 3, - "column": 27 - }, - "fullEnd": 170, - "start": 167, - "end": 169, - "expression": { - "id": 26, - "kind": "", - "startPos": { - "offset": 167, - "line": 3, - "column": 25 - }, - "fullStart": 167, - "endPos": { - "offset": 169, - "line": 3, - "column": 27 - }, - "fullEnd": 170, - "start": 167, - "end": 169, - "variable": { - "kind": "", - "startPos": { - "offset": 167, - "line": 3, - "column": 25 - }, - "endPos": { - "offset": 169, - "line": 3, - "column": 27 - }, - "value": "of", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 169, - "line": 3, - "column": 27 - }, - "endPos": { - "offset": 170, - "line": 3, - "column": 28 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 169, - "end": 170 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 167, - "end": 169 - } - } - }, - { - "id": 29, - "kind": "", - "startPos": { - "offset": 170, - "line": 3, - "column": 28 - }, - "fullStart": 170, - "endPos": { - "offset": 174, - "line": 3, - "column": 32 - }, - "fullEnd": 175, - "start": 170, - "end": 174, - "expression": { - "id": 28, - "kind": "", - "startPos": { - "offset": 170, - "line": 3, - "column": 28 - }, - "fullStart": 170, - "endPos": { - "offset": 174, - "line": 3, - "column": 32 - }, - "fullEnd": 175, - "start": 170, - "end": 174, - "variable": { - "kind": "", - "startPos": { - "offset": 170, - "line": 3, - "column": 28 - }, - "endPos": { - "offset": 174, - "line": 3, - "column": 32 - }, - "value": "args", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 174, - "line": 3, - "column": 32 - }, - "endPos": { - "offset": 175, - "line": 3, - "column": 33 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 174, - "end": 175 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 170, - "end": 174 - } - } - }, - { - "id": 30, - "kind": "", - "startPos": { - "offset": 175, - "line": 3, - "column": 33 - }, - "fullStart": 175, - "endPos": { - "offset": 177, - "line": 3, - "column": 35 - }, - "fullEnd": 212, - "start": 175, - "end": 177, - "blockOpenBrace": { - "kind": "", - "startPos": { - "offset": 175, - "line": 3, - "column": 33 - }, - "endPos": { - "offset": 176, - "line": 3, - "column": 34 - }, - "value": "{", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 175, - "end": 176 - }, - "body": [], - "blockCloseBrace": { - "kind": "", - "startPos": { - "offset": 176, - "line": 3, - "column": 34 - }, - "endPos": { - "offset": 177, - "line": 3, - "column": 35 - }, - "value": "}", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 177, - "line": 3, - "column": 35 - }, - "endPos": { - "offset": 178, - "line": 3, - "column": 36 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 177, - "end": 178 - }, - { - "kind": "", - "startPos": { - "offset": 178, - "line": 3, - "column": 36 - }, - "endPos": { - "offset": 211, - "line": 3, - "column": 69 - }, - "value": " this is also treated as column", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 178, - "end": 211 - }, - { - "kind": "", - "startPos": { - "offset": 211, - "line": 3, - "column": 69 - }, - "endPos": { - "offset": 212, - "line": 4, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 211, - "end": 212 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 176, - "end": 177 - } - } - ], - "symbol": 4 - }, - "start": 146, - "end": 177, - "name": "CompileError" - }, - { - "code": 3023, - "diagnostic": "Duplicate column Indexes", - "nodeOrToken": { - "id": 11, - "kind": "", - "startPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "fullStart": 10, - "endPos": { - "offset": 42, - "line": 1, - "column": 32 - }, - "fullEnd": 86, - "start": 14, - "end": 42, - "callee": { - "id": 3, - "kind": "", - "startPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "fullStart": 10, - "endPos": { - "offset": 21, - "line": 1, - "column": 11 - }, - "fullEnd": 22, - "start": 14, - "end": 21, - "expression": { - "id": 2, - "kind": "", - "startPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "fullStart": 10, - "endPos": { - "offset": 21, - "line": 1, - "column": 11 - }, - "fullEnd": 22, - "start": 14, - "end": 21, - "variable": { - "kind": "", - "startPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "endPos": { - "offset": 21, - "line": 1, - "column": 11 - }, - "value": "Indexes", - "leadingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 10, - "line": 1, - "column": 0 - }, - "endPos": { - "offset": 11, - "line": 1, - "column": 1 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 10, - "end": 11 - }, - { - "kind": "", - "startPos": { - "offset": 11, - "line": 1, - "column": 1 - }, - "endPos": { - "offset": 12, - "line": 1, - "column": 2 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 11, - "end": 12 - }, - { - "kind": "", - "startPos": { - "offset": 12, - "line": 1, - "column": 2 - }, - "endPos": { - "offset": 13, - "line": 1, - "column": 3 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 12, - "end": 13 - }, - { - "kind": "", - "startPos": { - "offset": 13, - "line": 1, - "column": 3 - }, - "endPos": { - "offset": 14, - "line": 1, - "column": 4 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 13, - "end": 14 - } - ], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 21, - "line": 1, - "column": 11 - }, - "endPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 21, - "end": 22 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 14, - "end": 21 - } - } - }, - "args": [ - { - "id": 5, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "fullStart": 22, - "endPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "fullEnd": 28, - "start": 22, - "end": 27, - "expression": { - "id": 4, - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "fullStart": 22, - "endPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "fullEnd": 28, - "start": 22, - "end": 27, - "variable": { - "kind": "", - "startPos": { - "offset": 22, - "line": 1, - "column": 12 - }, - "endPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "value": "wrong", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 27, - "line": 1, - "column": 17 - }, - "endPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 27, - "end": 28 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 22, - "end": 27 - } - } - }, - { - "id": 7, - "kind": "", - "startPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "fullStart": 28, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 34, - "start": 28, - "end": 33, - "expression": { - "id": 6, - "kind": "", - "startPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "fullStart": 28, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "fullEnd": 34, - "start": 28, - "end": 33, - "variable": { - "kind": "", - "startPos": { - "offset": 28, - "line": 1, - "column": 18 - }, - "endPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "value": "index", - "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 33, - "line": 1, - "column": 23 - }, - "endPos": { - "offset": 34, - "line": 1, - "column": 24 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 33, - "end": 34 - } - ], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 28, - "end": 33 - } - } + "fullEnd": 175, + "start": 170, + "end": 174, + "expression": { + "id": 28, + "kind": "", + "startPos": { + "offset": 170, + "line": 3, + "column": 28 }, - { - "id": 9, - "kind": "", + "fullStart": 170, + "endPos": { + "offset": 174, + "line": 3, + "column": 32 + }, + "fullEnd": 175, + "start": 170, + "end": 174, + "variable": { + "kind": "", "startPos": { - "offset": 34, - "line": 1, - "column": 24 + "offset": 170, + "line": 3, + "column": 28 }, - "fullStart": 34, "endPos": { - "offset": 39, - "line": 1, - "column": 29 + "offset": 174, + "line": 3, + "column": 32 }, - "fullEnd": 40, - "start": 34, - "end": 39, - "expression": { - "id": 8, - "kind": "", - "startPos": { - "offset": 34, - "line": 1, - "column": 24 - }, - "fullStart": 34, - "endPos": { - "offset": 39, - "line": 1, - "column": 29 - }, - "fullEnd": 40, - "start": 34, - "end": 39, - "variable": { - "kind": "", + "value": "args", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", "startPos": { - "offset": 34, - "line": 1, - "column": 24 + "offset": 174, + "line": 3, + "column": 32 }, "endPos": { - "offset": 39, - "line": 1, - "column": 29 + "offset": 175, + "line": 3, + "column": 33 }, - "value": "alias", + "value": " ", "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 39, - "line": 1, - "column": 29 - }, - "endPos": { - "offset": 40, - "line": 1, - "column": 30 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 39, - "end": 40 - } - ], + "trailingTrivia": [], "leadingInvalid": [], "trailingInvalid": [], "isInvalid": false, - "start": 34, - "end": 39 + "start": 174, + "end": 175 } - } + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 170, + "end": 174 }, - { - "id": 10, - "kind": "", - "startPos": { - "offset": 40, - "line": 1, - "column": 30 - }, - "fullStart": 40, - "endPos": { - "offset": 42, - "line": 1, - "column": 32 + "parent": 29 + }, + "parent": 31 + }, + "start": 170, + "end": 174, + "name": "CompileError" + }, + { + "code": 3019, + "diagnostic": "These fields must be some inline settings optionally ended with a setting list", + "nodeOrToken": { + "id": 30, + "kind": "", + "startPos": { + "offset": 175, + "line": 3, + "column": 33 + }, + "fullStart": 175, + "endPos": { + "offset": 177, + "line": 3, + "column": 35 + }, + "fullEnd": 212, + "start": 175, + "end": 177, + "blockOpenBrace": { + "kind": "", + "startPos": { + "offset": 175, + "line": 3, + "column": 33 + }, + "endPos": { + "offset": 176, + "line": 3, + "column": 34 + }, + "value": "{", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 175, + "end": 176 + }, + "body": [], + "blockCloseBrace": { + "kind": "", + "startPos": { + "offset": 176, + "line": 3, + "column": 34 + }, + "endPos": { + "offset": 177, + "line": 3, + "column": 35 + }, + "value": "}", + "leadingTrivia": [], + "trailingTrivia": [ + { + "kind": "", + "startPos": { + "offset": 177, + "line": 3, + "column": 35 + }, + "endPos": { + "offset": 178, + "line": 3, + "column": 36 + }, + "value": " ", + "leadingTrivia": [], + "trailingTrivia": [], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 177, + "end": 178 }, - "fullEnd": 86, - "start": 40, - "end": 42, - "blockOpenBrace": { - "kind": "", + { + "kind": "", "startPos": { - "offset": 40, - "line": 1, - "column": 30 + "offset": 178, + "line": 3, + "column": 36 }, "endPos": { - "offset": 41, - "line": 1, - "column": 31 + "offset": 211, + "line": 3, + "column": 69 }, - "value": "{", + "value": " this is also treated as column", "leadingTrivia": [], "trailingTrivia": [], "leadingInvalid": [], "trailingInvalid": [], "isInvalid": false, - "start": 40, - "end": 41 + "start": 178, + "end": 211 }, - "body": [], - "blockCloseBrace": { - "kind": "", + { + "kind": "", "startPos": { - "offset": 41, - "line": 1, - "column": 31 + "offset": 211, + "line": 3, + "column": 69 }, "endPos": { - "offset": 42, - "line": 1, - "column": 32 + "offset": 212, + "line": 4, + "column": 0 }, - "value": "}", + "value": "\n", "leadingTrivia": [], - "trailingTrivia": [ - { - "kind": "", - "startPos": { - "offset": 42, - "line": 1, - "column": 32 - }, - "endPos": { - "offset": 43, - "line": 1, - "column": 33 - }, - "value": " ", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 42, - "end": 43 - }, - { - "kind": "", - "startPos": { - "offset": 43, - "line": 1, - "column": 33 - }, - "endPos": { - "offset": 85, - "line": 1, - "column": 75 - }, - "value": " this indexes is treated as Table column", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 43, - "end": 85 - }, - { - "kind": "", - "startPos": { - "offset": 85, - "line": 1, - "column": 75 - }, - "endPos": { - "offset": 86, - "line": 2, - "column": 0 - }, - "value": "\n", - "leadingTrivia": [], - "trailingTrivia": [], - "leadingInvalid": [], - "trailingInvalid": [], - "isInvalid": false, - "start": 85, - "end": 86 - } - ], + "trailingTrivia": [], "leadingInvalid": [], "trailingInvalid": [], "isInvalid": false, - "start": 41, - "end": 42 + "start": 211, + "end": 212 } - } - ], - "symbol": 2 + ], + "leadingInvalid": [], + "trailingInvalid": [], + "isInvalid": false, + "start": 176, + "end": 177 + }, + "parent": 31 }, - "start": 14, - "end": 42, + "start": 175, + "end": 177, "name": "CompileError" } ] -} \ No newline at end of file +} diff --git a/packages/dbml-parse/__tests__/snapshots/validator/validator.test.ts b/packages/dbml-parse/__tests__/snapshots/validator/validator.test.ts index 4e2a8f193..5bb4256a9 100644 --- a/packages/dbml-parse/__tests__/snapshots/validator/validator.test.ts +++ b/packages/dbml-parse/__tests__/snapshots/validator/validator.test.ts @@ -1,31 +1,23 @@ import { readFileSync } from 'fs'; import path from 'path'; import { describe, expect, it } from 'vitest'; -import { NodeSymbolIdGenerator } from '@/core/analyzer/symbol/symbols'; -import { SyntaxNodeIdGenerator } from '@/core/parser/nodes'; -import Lexer from '@/core/lexer/lexer'; -import Parser from '@/core/parser/parser'; -import Validator from '@/core/analyzer/validator/validator'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { serialize, scanTestNames } from '@tests/utils'; +import { Compiler } from '@/index'; +import { serializeValidation, scanTestNames, stripIds, stripUnstableFields } from '@tests/utils'; describe('[snapshot] validator', () => { const testNames = scanTestNames(path.resolve(__dirname, './input/')); testNames.forEach((testName) => { const program = readFileSync(path.resolve(__dirname, `./input/${testName}.in.dbml`), 'utf-8'); - const symbolIdGenerator = new NodeSymbolIdGenerator(); - const nodeIdGenerator = new SyntaxNodeIdGenerator(); - const report = new Lexer(program) - .lex() - .chain((tokens) => { - return new Parser(program, tokens, nodeIdGenerator).parse(); - }) - .chain(({ ast }) => { - return new Validator(ast, new SymbolFactory(symbolIdGenerator)).validate(); - }); - const output = serialize(report, true); + const compiler = new Compiler(); + compiler.setSource(program); + const output = serializeValidation(compiler, true); - it(testName, () => expect(output).toMatchFileSnapshot(path.resolve(__dirname, `./output/${testName}.out.json`))); + it(testName, () => { + const expectedPath = path.resolve(__dirname, `./output/${testName}.out.json`); + const expectedRaw = readFileSync(expectedPath, 'utf-8'); + // Strip unstable node IDs and unstable fields from both sides before comparison + expect(stripIds(output)).toBe(stripIds(stripUnstableFields(expectedRaw))); + }); }); }); diff --git a/packages/dbml-parse/__tests__/utils/compiler.ts b/packages/dbml-parse/__tests__/utils/compiler.ts index 8f576747e..d323e5d7f 100644 --- a/packages/dbml-parse/__tests__/utils/compiler.ts +++ b/packages/dbml-parse/__tests__/utils/compiler.ts @@ -1,10 +1,9 @@ -import Lexer from '@/core/lexer/lexer'; -import Parser from '@/core/parser/parser'; -import Analyzer from '@/core/analyzer/analyzer'; +import Lexer from '@/core/syntax/lexer/lexer'; +import { SyntaxNodeIdGenerator } from '@/core/types/nodes'; +import Parser from '@/core/syntax/parser/parser'; import { ProgramNode, SyntaxNode, - SyntaxNodeIdGenerator, SyntaxNodeKind, ElementDeclarationNode, AttributeNode, @@ -23,11 +22,11 @@ import { VariableNode, PrimaryExpressionNode, ArrayNode, -} from '@/core/parser/nodes'; -import { NodeSymbolIdGenerator } from '@/core/analyzer/symbol/symbols'; -import Report from '@/core/report'; +} from '@/core/types/nodes'; +import Report from '@/core/types/report'; import { Compiler, SyntaxToken } from '@/index'; -import { Database } from '@/core/interpreter/types'; +import { Database } from '@/core/types/schemaJson'; +import { UNHANDLED } from '@/constants'; export function lex (source: string): Report { return new Lexer(source).lex(); @@ -37,14 +36,34 @@ export function parse (source: string): Report<{ ast: ProgramNode; tokens: Synta return new Lexer(source).lex().chain((tokens) => new Parser(source, tokens, new SyntaxNodeIdGenerator()).parse()); } -export function analyze (source: string): Report { - return parse(source).chain(({ ast }) => new Analyzer(ast, new NodeSymbolIdGenerator()).analyze()); +export function analyze (source: string) { + const compiler = new Compiler(); + compiler.setSource(source); + const parseResult = compiler.parseFile(); + const ast = parseResult.getValue().ast; + const bindResult = compiler.bind(ast); + const validateResult = compiler.validate(ast); + const errors = [...parseResult.getErrors(), ...bindResult.getErrors(), ...validateResult.getErrors()]; + const warnings = [...parseResult.getWarnings(), ...bindResult.getWarnings(), ...validateResult.getWarnings()]; + return { + getErrors: () => errors, + getWarnings: () => warnings, + compiler, + }; } export function interpret (source: string): Report { const compiler = new Compiler(); compiler.setSource(source); - return compiler.parse._().map(({ rawDb }) => rawDb); + const ast = compiler.parseFile().getValue().ast; + const bindResult = compiler.bind(ast); + const interpretResult = compiler.interpret(ast); + const db = interpretResult.getValue(); + return new Report( + db instanceof Database ? db : undefined, + [...bindResult.getErrors(), ...interpretResult.getErrors()], + [...bindResult.getWarnings(), ...interpretResult.getWarnings()], + ); } export function flattenTokens (token: SyntaxToken): SyntaxToken[] { diff --git a/packages/dbml-parse/__tests__/utils/index.ts b/packages/dbml-parse/__tests__/utils/index.ts index 278098e33..ea193144a 100644 --- a/packages/dbml-parse/__tests__/utils/index.ts +++ b/packages/dbml-parse/__tests__/utils/index.ts @@ -11,7 +11,10 @@ export { // Test helpers for snapshot testing export { scanTestNames, - serialize, + serializeAnalysis, + serializeValidation, + stripIds, + stripUnstableFields, } from './testHelpers'; // Compiler utilities for property testing diff --git a/packages/dbml-parse/__tests__/utils/testHelpers.ts b/packages/dbml-parse/__tests__/utils/testHelpers.ts index 2d2a122b6..c2bdee00d 100644 --- a/packages/dbml-parse/__tests__/utils/testHelpers.ts +++ b/packages/dbml-parse/__tests__/utils/testHelpers.ts @@ -1,6 +1,5 @@ -import { NodeSymbol } from '@/core/analyzer/symbol/symbols'; -import Report from '@/core/report'; import { ProgramNode, SyntaxNode } from '@/index'; +import type Compiler from '@/compiler/index'; import fs from 'fs'; export function scanTestNames (_path: any) { @@ -10,66 +9,48 @@ export function scanTestNames (_path: any) { } /** - * Serializes a compiler report to JSON, handling circular references and - * reducing verbosity by outputting IDs instead of full objects where appropriate. - * - * The serializer handles special keys: - * - 'symbol': For non-root nodes, outputs only the symbol ID. For root nodes, - * outputs the full symbol table with references as IDs. - * - 'referee': Outputs only the referenced symbol's ID - * - 'parent': Outputs only the parent node's ID - * - 'declaration': Outputs only the declaration node's ID - * - 'symbolTable': Converts Map to Object for JSON compatibility + * JSON replacer that strips unstable fields (parent, source, symbol, references, referee) + * and internal fields (starting with _) from serialized output. */ -export function serialize ( - report: Readonly>, - pretty: boolean = false, -): string { - return JSON.stringify( - report, - function (key: string, value: any) { - // For non-root nodes: output just the symbol's ID (avoids circular refs) - if (!(this instanceof ProgramNode) && key === 'symbol') { - return (value as NodeSymbol)?.id; - } - - // Don't include source in the serialized AST - if (this instanceof ProgramNode && key === 'source') { - return undefined; - } - - // For root node symbol: output full symbol table with reference IDs - if (key === 'symbol') { - return { - symbolTable: (value as NodeSymbol)?.symbolTable, - id: (value as NodeSymbol)?.id, - references: (value as NodeSymbol)?.references.map((ref) => ref.id), - declaration: (value as NodeSymbol)?.declaration?.id, - }; - } - - // For referee references: output only the symbol ID - if (key === 'referee') { - return (value as NodeSymbol)?.id; - } +function stableReplacer (key: string, value: any): any { + if (['parent', 'source', 'symbol', 'references', 'referee'].includes(key)) return undefined; + if (key.startsWith('_')) return undefined; + return value; +} - // For parent references: output only the node ID (avoids circular refs) - if (key === 'parent') { - return (value as SyntaxNode)?.id; - } +/** + * Strips unstable node IDs from a JSON string. + */ +export function stripIds (s: string): string { + return s.replace(/"id": \d+,?\n/g, ''); +} - // For declaration references: output only the node ID - if (key === 'declaration') { - return (value as SyntaxNode)?.id; - } +/** + * Re-serializes a JSON string using the stable replacer to strip unstable fields. + */ +export function stripUnstableFields (jsonStr: string): string { + return JSON.stringify(JSON.parse(jsonStr), stableReplacer, 2); +} - // For symbol tables: convert Map to Object for JSON serialization - if (key === 'symbolTable') { - return Object.fromEntries((value as any).table); - } +/** + * Serializes analysis (bind) results from a Compiler to JSON. + * The compiler's query-based system stores symbol/referee data on the nodes directly, + * so we just serialize the AST with errors. + */ +export function serializeAnalysis (compiler: Compiler, pretty = false): string { + const ast = compiler.parseFile().getValue().ast; + const bindResult = compiler.bind(ast); + const errors = [...compiler.parseFile().getErrors(), ...bindResult.getErrors()]; + return JSON.stringify({ value: ast, errors }, stableReplacer, pretty ? 2 : 0); +} - return value; - }, - pretty ? 2 : 0, - ); +/** + * Serializes validation-only results from a Compiler to JSON. + * Only includes validation errors (no binding/referee data). + */ +export function serializeValidation (compiler: Compiler, pretty = false): string { + const ast = compiler.parseFile().getValue().ast; + const validateResult = compiler.validate(ast); + const errors = [...compiler.parseFile().getErrors(), ...validateResult.getErrors()]; + return JSON.stringify({ value: ast, errors }, stableReplacer, pretty ? 2 : 0); } diff --git a/packages/dbml-parse/src/compiler/index.ts b/packages/dbml-parse/src/compiler/index.ts index 351f9f1fa..2759d212b 100644 --- a/packages/dbml-parse/src/compiler/index.ts +++ b/packages/dbml-parse/src/compiler/index.ts @@ -1,114 +1,111 @@ -import { SyntaxNodeIdGenerator, ProgramNode } from '@/core/parser/nodes'; -import { NodeSymbolIdGenerator } from '@/core/analyzer/symbol/symbols'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { Database } from '@/core/interpreter/types'; -import Report from '@/core/report'; -import Lexer from '@/core/lexer/lexer'; -import Parser from '@/core/parser/parser'; -import Analyzer from '@/core/analyzer/analyzer'; -import Interpreter from '@/core/interpreter/interpreter'; -import { DBMLCompletionItemProvider, DBMLDefinitionProvider, DBMLReferencesProvider, DBMLDiagnosticsProvider } from '@/services/index'; -import { ast, errors, warnings, tokens, rawDb, publicSymbolTable } from './queries/parse'; +// Lazy import: services depend on modules not yet migrated +// import { DBMLCompletionItemProvider, DBMLDefinitionProvider, DBMLReferencesProvider, DBMLDiagnosticsProvider } from '@/services/index'; import { invalidStream, flatStream } from './queries/token'; -import { symbolOfName, symbolOfNameToKey, symbolMembers } from './queries/symbol'; -import { containerStack, containerToken, containerElement, containerScope, containerScopeKind } from './queries/container'; -import { - renameTable, - applyTextEdits, - type TextEdit, - type TableNameInput, -} from './queries/transform'; import { splitQualifiedIdentifier, unescapeString, escapeString, formatRecordValue, isValidIdentifier, addDoubleQuoteIfNeeded } from './queries/utils'; - -// Re-export types +import { parseFile } from './queries/pipeline'; +import { containerStack, containerToken, containerElement, containerScope, containerScopeKind } from './queries/container'; +import { renameTable, type TableNameInput } from './queries/transform'; export { ScopeKind } from './types'; -export type { TextEdit, TableNameInput }; +export { type TextEdit, type TableNameInput } from './queries/transform'; +import { + nodeSymbol, + symbolMembers, + nodeReferee, + nestedSymbols, + bind, + interpret, +} from '@/core/global_modules'; +import { symbolReferences } from './queries/symbolReferences'; +import { intern, Internable, Primitive } from '@/core/types/internable'; +import { UNHANDLED } from '@/constants'; +import { alias, fullname, settings, validate } from '@/core/syntax/local_modules'; // Re-export utilities export { splitQualifiedIdentifier, unescapeString, escapeString, formatRecordValue, isValidIdentifier, addDoubleQuoteIfNeeded }; +const COMPUTING = Symbol('COMPUTING'); + export default class Compiler { private source = ''; private cache = new Map(); - private nodeIdGenerator = new SyntaxNodeIdGenerator(); - private symbolIdGenerator = new NodeSymbolIdGenerator(); setSource (source: string) { this.source = source; this.cache.clear(); - this.nodeIdGenerator.reset(); - this.symbolIdGenerator.reset(); } - private query ( + private query)[], Return> ( fn: (this: Compiler, ...args: Args) => Return, - toKey?: (...args: Args) => unknown, ): (...args: Args) => Return { - const cacheKey = Symbol(); + const queryKey = Symbol(); return ((...args: Args): Return => { - if (args.length === 0) { - if (this.cache.has(cacheKey)) return this.cache.get(cacheKey); - const result = fn.apply(this, args); - this.cache.set(cacheKey, result); - return result; + const argKey = args.map((a) => intern(a)).join('\0'); + let subCache = this.cache.get(queryKey); + if (subCache instanceof Map) { + if (subCache.has(argKey)) { + const cached = subCache.get(argKey); + if (cached === COMPUTING) { + throw new Error(`Cycle detected in query: ${fn.name}(${argKey})`); + } + return cached; + } } - const key = toKey ? toKey(...args) : args[0]; - let mapCache = this.cache.get(cacheKey); - if (mapCache instanceof Map && mapCache.has(key)) return mapCache.get(key); + if (!(subCache instanceof Map)) { + subCache = new Map(); + this.cache.set(queryKey, subCache); + } + subCache.set(argKey, COMPUTING); const result = fn.apply(this, args); - if (!(mapCache instanceof Map)) { - mapCache = new Map(); - this.cache.set(cacheKey, mapCache); - } - mapCache.set(key, result); + subCache.set(argKey, result); return result; }) as (...args: Args) => Return; } - private interpret (): Report<{ ast: ProgramNode; tokens: SyntaxToken[]; rawDb?: Database }> { - const parseRes: Report<{ ast: ProgramNode; tokens: SyntaxToken[] }> = new Lexer(this.source) - .lex() - .chain((lexedTokens) => new Parser(this.source, lexedTokens as SyntaxToken[], this.nodeIdGenerator).parse()) - .chain(({ ast, tokens }) => new Analyzer(ast, this.symbolIdGenerator).analyze().map(() => ({ ast, tokens }))); - - if (parseRes.getErrors().length > 0) { - return parseRes as Report<{ ast: ProgramNode; tokens: SyntaxToken[]; rawDb?: Database }>; - } - - return parseRes.chain(({ ast, tokens }) => - new Interpreter(ast).interpret().map((rawDb) => ({ ast, tokens, rawDb })), - ); - } + parseFile = this.query(parseFile); + nodeSymbol = this.query(nodeSymbol); + symbolMembers = this.query(symbolMembers); + symbolReferences = this.query(symbolReferences); + nodeReferee = this.query(nodeReferee); + nestedSymbols = this.query(nestedSymbols); + bind = this.query(bind); + interpret = this.query(interpret); - renameTable ( - oldName: TableNameInput, - newName: TableNameInput, - ): string { + renameTable (oldName: TableNameInput, newName: TableNameInput): string { return renameTable.call(this, oldName, newName); } - applyTextEdits (edits: TextEdit[]): string { - return applyTextEdits(this.parse.source(), edits); - } + validate = this.query(validate); + fullname = this.query(fullname); + alias = this.query(alias); + settings = this.query(settings); readonly token = { invalidStream: this.query(invalidStream), flatStream: this.query(flatStream), }; + // @deprecated - legacy APIs for services compatibility readonly parse = { source: () => this.source as Readonly, - _: this.query(this.interpret), - ast: this.query(ast), - errors: this.query(errors), - warnings: this.query(warnings), - tokens: this.query(tokens), - rawDb: this.query(rawDb), - publicSymbolTable: this.query(publicSymbolTable), + ast: () => this.parseFile().getValue().ast, + _: () => { + const ast = this.parseFile().getValue().ast; + this.bind(ast); + return this.interpret(ast); + }, + publicSymbolTable: () => { + const ast = this.parseFile().getValue().ast; + const sym = nodeSymbol.call(this, ast); + if (sym.hasValue(UNHANDLED)) return undefined; + const members = symbolMembers.call(this, sym.getValue()); + if (members.hasValue(UNHANDLED)) return undefined; + return members.getValue(); + }, }; + // @deprecated readonly container = { stack: this.query(containerStack), token: this.query(containerToken), @@ -117,12 +114,9 @@ export default class Compiler { scopeKind: this.query(containerScopeKind), }; - readonly symbol = { - ofName: this.query(symbolOfName, symbolOfNameToKey), - members: this.query(symbolMembers), - }; - initMonacoServices () { + async initMonacoServices () { + const { DBMLCompletionItemProvider, DBMLDefinitionProvider, DBMLReferencesProvider, DBMLDiagnosticsProvider } = await import('@/services/index'); return { definitionProvider: new DBMLDefinitionProvider(this), referenceProvider: new DBMLReferencesProvider(this), diff --git a/packages/dbml-parse/src/compiler/queries/container/element.ts b/packages/dbml-parse/src/compiler/queries/container/element.ts index 52deea8cb..b3ab60a60 100644 --- a/packages/dbml-parse/src/compiler/queries/container/element.ts +++ b/packages/dbml-parse/src/compiler/queries/container/element.ts @@ -1,5 +1,5 @@ import type Compiler from '../../index'; -import { ElementDeclarationNode, ProgramNode } from '@/core/parser/nodes'; +import { ElementDeclarationNode, ProgramNode } from '@/core/types/nodes'; export function containerElement ( this: Compiler, diff --git a/packages/dbml-parse/src/compiler/queries/container/scope.ts b/packages/dbml-parse/src/compiler/queries/container/scope.ts index 1026d268c..65c172f7f 100644 --- a/packages/dbml-parse/src/compiler/queries/container/scope.ts +++ b/packages/dbml-parse/src/compiler/queries/container/scope.ts @@ -1,6 +1,15 @@ import type Compiler from '../../index'; -import type SymbolTable from '@/core/analyzer/symbol/symbolTable'; +import { NodeSymbol } from '@/core/types/symbols'; +import { UNHANDLED } from '@/constants'; +import { nodeSymbol, symbolMembers } from '@/core/global_modules'; -export function containerScope (this: Compiler, offset: number): Readonly | undefined { - return this.container.element(offset)?.symbol?.symbolTable; +// @deprecated - returns the members of the element at offset +export function containerScope (this: Compiler, offset: number): NodeSymbol[] | undefined { + const element = this.container.element(offset); + if (!element) return undefined; + const sym = nodeSymbol.call(this, element); + if (sym.hasValue(UNHANDLED)) return undefined; + const members = symbolMembers.call(this, sym.getValue()); + if (members.hasValue(UNHANDLED)) return undefined; + return members.getValue(); } diff --git a/packages/dbml-parse/src/compiler/queries/container/scopeKind.ts b/packages/dbml-parse/src/compiler/queries/container/scopeKind.ts index 9c4358873..970b80f97 100644 --- a/packages/dbml-parse/src/compiler/queries/container/scopeKind.ts +++ b/packages/dbml-parse/src/compiler/queries/container/scopeKind.ts @@ -1,6 +1,6 @@ import type Compiler from '../../index'; import { ScopeKind } from '../../types'; -import { ElementDeclarationNode, ProgramNode } from '@/core/parser/nodes'; +import { ElementDeclarationNode, ProgramNode } from '@/core/types/nodes'; export function containerScopeKind (this: Compiler, offset: number): ScopeKind { const elem = this.container.element(offset); diff --git a/packages/dbml-parse/src/compiler/queries/container/stack.ts b/packages/dbml-parse/src/compiler/queries/container/stack.ts index 0486d2710..a23e38356 100644 --- a/packages/dbml-parse/src/compiler/queries/container/stack.ts +++ b/packages/dbml-parse/src/compiler/queries/container/stack.ts @@ -11,10 +11,10 @@ import { CommaExpressionNode, BlockExpressionNode, IdentiferStreamNode, -} from '@/core/parser/nodes'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; -import { isOffsetWithinSpan } from '@/core/utils'; -import { getMemberChain } from '@/core/parser/utils'; +} from '@/core/types/nodes'; +import { SyntaxToken, SyntaxTokenKind } from '@/core/types/tokens'; +import { isOffsetWithinSpan } from '@/core/utils/span'; +import { getMemberChain } from '@/core/syntax/parser/utils'; export function containerStack (this: Compiler, offset: number): readonly Readonly[] { const tokens = this.token.flatStream(); diff --git a/packages/dbml-parse/src/compiler/queries/container/token.ts b/packages/dbml-parse/src/compiler/queries/container/token.ts index bdf3cb294..baed0d783 100644 --- a/packages/dbml-parse/src/compiler/queries/container/token.ts +++ b/packages/dbml-parse/src/compiler/queries/container/token.ts @@ -1,5 +1,5 @@ import type Compiler from '../../index'; -import type { SyntaxToken } from '@/core/lexer/tokens'; +import type { SyntaxToken } from '@/core/types/tokens'; export function containerToken (this: Compiler, offset: number): { token: SyntaxToken; index: number } | { token: undefined; index: undefined } { const id = this.token.flatStream().findIndex((t) => t.start >= offset); diff --git a/packages/dbml-parse/src/compiler/queries/parse.ts b/packages/dbml-parse/src/compiler/queries/parse.ts deleted file mode 100644 index 14936d8e2..000000000 --- a/packages/dbml-parse/src/compiler/queries/parse.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type Compiler from '../index'; -import type { ProgramNode } from '@/core/parser/nodes'; -import type { SyntaxToken } from '@/core/lexer/tokens'; -import type { CompileError, CompileWarning } from '@/core/errors'; -import type { Database } from '@/core/interpreter/types'; -import type SymbolTable from '@/core/analyzer/symbol/symbolTable'; - -export function ast (this: Compiler): Readonly { - return this.parse._().getValue().ast; -} - -export function errors (this: Compiler): readonly Readonly[] { - return this.parse._().getErrors(); -} - -export function warnings (this: Compiler): readonly Readonly[] { - return this.parse._().getWarnings(); -} - -export function tokens (this: Compiler): Readonly[] { - return this.parse._().getValue().tokens; -} - -export function rawDb (this: Compiler): Readonly | undefined { - return this.parse._().getValue().rawDb; -} - -export function publicSymbolTable (this: Compiler): Readonly { - return this.parse._().getValue().ast.symbol!.symbolTable!; -} diff --git a/packages/dbml-parse/src/compiler/queries/pipeline/index.ts b/packages/dbml-parse/src/compiler/queries/pipeline/index.ts new file mode 100644 index 000000000..2978498e2 --- /dev/null +++ b/packages/dbml-parse/src/compiler/queries/pipeline/index.ts @@ -0,0 +1 @@ +export { parseFile } from './parse'; diff --git a/packages/dbml-parse/src/compiler/queries/pipeline/parse.ts b/packages/dbml-parse/src/compiler/queries/pipeline/parse.ts new file mode 100644 index 000000000..cc2ba3cdb --- /dev/null +++ b/packages/dbml-parse/src/compiler/queries/pipeline/parse.ts @@ -0,0 +1,17 @@ +import type Compiler from '../../index'; +import type { ProgramNode } from '@/core/types/nodes'; +import type { SyntaxToken } from '@/core/types/tokens'; +import { SyntaxNodeIdGenerator } from '@/core/types/nodes'; +import Report from '@/core/types/report'; +import Lexer from '@/core/syntax/lexer/lexer'; +import Parser from '@/core/syntax/parser/parser'; + +export function parseFile (this: Compiler): Report<{ + readonly ast: Readonly; + readonly tokens: readonly Readonly[]; +}> { + const source = this.parse.source(); + return new Lexer(source) + .lex() + .chain((lexedTokens) => new Parser(source, lexedTokens as SyntaxToken[], new SyntaxNodeIdGenerator()).parse()); +} diff --git a/packages/dbml-parse/src/compiler/queries/scope.ts b/packages/dbml-parse/src/compiler/queries/scope.ts new file mode 100644 index 000000000..1925a1e80 --- /dev/null +++ b/packages/dbml-parse/src/compiler/queries/scope.ts @@ -0,0 +1,18 @@ +import type Compiler from '../index'; +import { ElementDeclarationNode, ProgramNode, SyntaxNode } from '@/core/types/nodes'; + +export function scope ( + this: Compiler, + node: SyntaxNode, +): Readonly { + let current: SyntaxNode | undefined = node.parent; + + while (current) { + if (current instanceof ElementDeclarationNode || current instanceof ProgramNode) { + return current; + } + current = current.parent; + } + + return this.parse.ast(); +} diff --git a/packages/dbml-parse/src/compiler/queries/symbol.ts b/packages/dbml-parse/src/compiler/queries/symbol.ts deleted file mode 100644 index dbb9d63a3..000000000 --- a/packages/dbml-parse/src/compiler/queries/symbol.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type Compiler from '../index'; -import { ElementDeclarationNode, ProgramNode } from '@/core/parser/nodes'; -import { NodeSymbol } from '@/core/analyzer/symbol/symbols'; -import { SymbolKind, destructureIndex } from '@/core/analyzer/symbol/symbolIndex'; -import { generatePossibleIndexes } from '@/core/analyzer/symbol/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; - -export function symbolMembers (this: Compiler, ownerSymbol: NodeSymbol) { - if (!ownerSymbol.symbolTable) { - return []; - } - - return [...ownerSymbol.symbolTable.entries()].map(([index, symbol]) => ({ - ...destructureIndex(index).unwrap(), - symbol, - })); -} - -export function symbolOfName (this: Compiler, nameStack: string[], owner: ElementDeclarationNode | ProgramNode) { - if (nameStack.length === 0) { - return []; - } - - const res: { symbol: NodeSymbol; kind: SymbolKind; name: string }[] = []; - - for ( - let currentOwner: ElementDeclarationNode | ProgramNode | undefined = owner; - currentOwner; - currentOwner = currentOwner instanceof ElementDeclarationNode - ? currentOwner.parent - : undefined - ) { - if (!currentOwner.symbol?.symbolTable) { - continue; - } - - const { symbolTable } = currentOwner.symbol; - let currentPossibleSymbolTables: SymbolTable[] = [symbolTable]; - let currentPossibleSymbols: { symbol: NodeSymbol; kind: SymbolKind; name: string }[] = []; - - for (const name of nameStack) { - currentPossibleSymbols = currentPossibleSymbolTables.flatMap((st) => - generatePossibleIndexes(name).flatMap((index) => { - const symbol = st.get(index); - const desRes = destructureIndex(index).unwrap_or(undefined); - - return !symbol || !desRes ? [] : { ...desRes, symbol }; - }), - ); - currentPossibleSymbolTables = currentPossibleSymbols.flatMap((e) => - e.symbol.symbolTable ? e.symbol.symbolTable : [], - ); - } - - res.push(...currentPossibleSymbols); - } - - return res; -} - -export function symbolOfNameToKey (nameStack: string[], owner: { id: number }): string { - return `${nameStack.join('.')}@${owner.id}`; -} diff --git a/packages/dbml-parse/src/compiler/queries/symbolReferences.ts b/packages/dbml-parse/src/compiler/queries/symbolReferences.ts new file mode 100644 index 000000000..c56632395 --- /dev/null +++ b/packages/dbml-parse/src/compiler/queries/symbolReferences.ts @@ -0,0 +1,33 @@ +import type Compiler from '../index'; +import { SyntaxNode, PrimaryExpressionNode } from '@/core/types/nodes'; +import { NodeSymbol } from '@/core/types/symbols'; +import { UNHANDLED } from '@/constants'; +import { isExpressionAVariableNode } from '@/core/utils/expression'; +import { getMemberChain } from '@/core/syntax/parser/utils'; +import Report from '@/core/types/report'; +import { nodeReferee } from '@/core/global_modules'; + +// Collect all AST nodes whose nodeReferee resolves to the given symbol. +// Walks every variable node checking the memoized nodeReferee result. +export function symbolReferences (this: Compiler, symbol: NodeSymbol): Report { + const ast = this.parseFile().getValue().ast; + this.bind(ast); + + const refs: SyntaxNode[] = []; + const walk = (node: SyntaxNode): void => { + if (isExpressionAVariableNode(node)) { + const refereeResult = nodeReferee.call(this, node); + if (refereeResult.hasValue(UNHANDLED)) return; + if (refereeResult.getValue() === symbol) { + refs.push(node); + } + return; + } + for (const child of getMemberChain(node)) { + if (child instanceof SyntaxNode) walk(child); + } + }; + walk(ast); + + return new Report(refs); +} diff --git a/packages/dbml-parse/src/compiler/queries/token.ts b/packages/dbml-parse/src/compiler/queries/token.ts index f6eef9817..d060b55d5 100644 --- a/packages/dbml-parse/src/compiler/queries/token.ts +++ b/packages/dbml-parse/src/compiler/queries/token.ts @@ -1,12 +1,12 @@ import type Compiler from '../index'; -import type { SyntaxToken } from '@/core/lexer/tokens'; -import { isInvalidToken } from '@/core/parser/utils'; +import type { SyntaxToken } from '@/core/types/tokens'; +import { isInvalidToken } from '@/core/syntax/parser/utils'; export function flatStream (this: Compiler): readonly SyntaxToken[] { - return this.parse.tokens() - .flatMap((token) => [...token.leadingInvalid, token, ...token.trailingInvalid]); + return (this.parseFile().getValue().tokens as SyntaxToken[]) + .flatMap((token: SyntaxToken) => [...token.leadingInvalid, token, ...token.trailingInvalid]); } export function invalidStream (this: Compiler): readonly SyntaxToken[] { - return this.parse.tokens().filter(isInvalidToken); + return (this.parseFile().getValue().tokens as SyntaxToken[]).filter(isInvalidToken); } diff --git a/packages/dbml-parse/src/compiler/queries/transform/renameTable.ts b/packages/dbml-parse/src/compiler/queries/transform/renameTable.ts index a84704dc4..b04b52473 100644 --- a/packages/dbml-parse/src/compiler/queries/transform/renameTable.ts +++ b/packages/dbml-parse/src/compiler/queries/transform/renameTable.ts @@ -1,11 +1,9 @@ import { DEFAULT_SCHEMA_NAME } from '@/constants'; import type Compiler from '../../index'; -import { SyntaxNode } from '@/core/parser/nodes'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { TableSymbol } from '@/core/analyzer/symbol/symbols'; -import { createSchemaSymbolIndex, createTableSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; +import { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol } from '@/core/types/symbols'; import { applyTextEdits, TextEdit } from './applyTextEdits'; -import { isAlphaOrUnderscore, isDigit } from '@/core/utils'; +import { isAlphaOrUnderscore, isDigit } from '@/core/utils/chars'; import { normalizeTableName, lookupTableSymbol, stripQuotes, type TableNameInput } from './utils'; interface FormattedTableName { @@ -30,7 +28,7 @@ function isValidIdentifier (name: string): boolean { * the source text at the declaration node position. */ function checkIfTableDeclarationUsesQuotes ( - tableSymbol: TableSymbol, + tableSymbol: NodeSymbol, source: string, ): boolean { if (!tableSymbol.declaration) { @@ -77,38 +75,15 @@ function formatTableName ( * Checks if renaming would cause a name collision. */ function checkForNameCollision ( - symbolTable: Readonly, + compiler: Compiler, oldSchema: string, oldTable: string, newSchema: string, newTable: string, ): boolean { - const tableSymbolIndex = createTableSymbolIndex(newTable); - let existingTableSymbol; - - if (newSchema === DEFAULT_SCHEMA_NAME) { - existingTableSymbol = symbolTable.get(tableSymbolIndex); - } else { - const schemaSymbolIndex = createSchemaSymbolIndex(newSchema); - const schemaSymbol = symbolTable.get(schemaSymbolIndex); - - if (!schemaSymbol || !schemaSymbol.symbolTable) { - return false; - } - - existingTableSymbol = schemaSymbol.symbolTable.get(tableSymbolIndex); - } - - if (!existingTableSymbol) { - return false; - } - - // Not a collision if renaming to the same name - if (oldSchema === newSchema && oldTable === newTable) { - return false; - } - - return true; + if (oldSchema === newSchema && oldTable === newTable) return false; + const existing = lookupTableSymbol(compiler, newSchema, newTable); + return existing !== null; } /** @@ -230,7 +205,6 @@ export function renameTable ( newName: TableNameInput, ): string { const source = this.parse.source(); - const symbolTable = this.parse.publicSymbolTable(); const normalizedOld = normalizeTableName(oldName); const normalizedNew = normalizeTableName(newName); @@ -241,13 +215,13 @@ export function renameTable ( const newTable = normalizedNew.table; // Look up the table symbol - const tableSymbol = lookupTableSymbol(symbolTable, oldSchema, oldTable); + const tableSymbol = lookupTableSymbol(this, oldSchema, oldTable); if (!tableSymbol) { return source; } // Check for name collision - if (checkForNameCollision(symbolTable, oldSchema, oldTable, newSchema, newTable)) { + if (checkForNameCollision(this, oldSchema, oldTable, newSchema, newTable)) { return source; } @@ -265,7 +239,9 @@ export function renameTable ( } } - for (const ref of tableSymbol.references) { + const referencesReport = this.symbolReferences(tableSymbol); + const references = referencesReport.getValue(); + for (const ref of references) { const refText = source.substring(ref.start, ref.end); const cleanRefText = refText.replace(/"/g, ''); if (cleanRefText === oldTable) { diff --git a/packages/dbml-parse/src/compiler/queries/transform/utils.ts b/packages/dbml-parse/src/compiler/queries/transform/utils.ts index e1fd6dcf0..0c026d910 100644 --- a/packages/dbml-parse/src/compiler/queries/transform/utils.ts +++ b/packages/dbml-parse/src/compiler/queries/transform/utils.ts @@ -1,8 +1,7 @@ -import { DEFAULT_SCHEMA_NAME } from '@/constants'; +import { DEFAULT_SCHEMA_NAME, UNHANDLED } from '@/constants'; import { splitQualifiedIdentifier } from '../utils'; -import { createTableSymbolIndex, createSchemaSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; -import type SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { TableSymbol } from '@/core/analyzer/symbol/symbols'; +import type Compiler from '../../index'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; export type TableNameInput = string | { schema?: string; table: string }; @@ -51,29 +50,46 @@ export function normalizeTableName (input: TableNameInput): { schema: string; ta } /** - * Looks up a table symbol from the symbol table. + * Looks up a table symbol by matching its full qualified name. */ export function lookupTableSymbol ( - symbolTable: Readonly, + compiler: Compiler, schema: string, table: string, -): TableSymbol | null { - const tableSymbolIndex = createTableSymbolIndex(table); +): NodeSymbol | null { + const publicSymbols = compiler.parse.publicSymbolTable(); + if (!publicSymbols) return null; - if (schema === DEFAULT_SCHEMA_NAME) { - const symbol = symbolTable.get(tableSymbolIndex); - return symbol instanceof TableSymbol ? symbol : null; - } + // Build the expected fullname + const expectedFullname = schema === DEFAULT_SCHEMA_NAME ? [table] : [schema, table]; - const schemaSymbolIndex = createSchemaSymbolIndex(schema); - const schemaSymbol = symbolTable.get(schemaSymbolIndex); + // First try by table name + const byName = publicSymbols.find((sym) => { + if (!sym.isKind(SymbolKind.Table)) return false; + if (!sym.declaration) return false; + const fn = compiler.fullname(sym.declaration); + if (fn.hasValue(UNHANDLED)) return false; + const parts = fn.getValue(); + if (!parts) return false; + const lastName = parts.at(-1); + const schemaPrefix = parts.length >= 2 ? parts[0] : DEFAULT_SCHEMA_NAME; + return lastName === table && schemaPrefix === schema; + }); + if (byName) return byName; - if (!schemaSymbol || !schemaSymbol.symbolTable) { - return null; + // Fall back to alias lookup (aliases are schema-independent) + if (schema === DEFAULT_SCHEMA_NAME) { + const byAlias = publicSymbols.find((sym) => { + if (!sym.isKind(SymbolKind.Table)) return false; + if (!sym.declaration) return false; + const aliasResult = compiler.alias(sym.declaration); + if (aliasResult.hasValue(UNHANDLED)) return false; + return aliasResult.getValue() === table; + }); + if (byAlias) return byAlias; } - const symbol = schemaSymbol.symbolTable.get(tableSymbolIndex); - return symbol instanceof TableSymbol ? symbol : null; + return null; } /** diff --git a/packages/dbml-parse/src/compiler/queries/utils.ts b/packages/dbml-parse/src/compiler/queries/utils.ts index 03150feb6..9de8ad82e 100644 --- a/packages/dbml-parse/src/compiler/queries/utils.ts +++ b/packages/dbml-parse/src/compiler/queries/utils.ts @@ -6,8 +6,8 @@ import { tryExtractNumeric, tryExtractString, tryExtractDateTime, -} from '@/core/interpreter/records/utils'; -import { isAlphaOrUnderscore, isDigit } from '@/core/utils'; +} from '@/core/global_modules/records/utils/data'; +import { isAlphaOrUnderscore, isDigit } from '@/core/utils/chars'; /** * Checks if an identifier is valid (can be used without quotes in DBML). diff --git a/packages/dbml-parse/src/constants.ts b/packages/dbml-parse/src/constants.ts index ab1dda4c1..1a0749424 100644 --- a/packages/dbml-parse/src/constants.ts +++ b/packages/dbml-parse/src/constants.ts @@ -1,3 +1,9 @@ export const KEYWORDS_OF_DEFAULT_SETTING = ['null', 'true', 'false'] as readonly string[]; export const NUMERIC_LITERAL_PREFIX = ['-', '+'] as readonly string[]; export const DEFAULT_SCHEMA_NAME = 'public'; + +export const PASS_THROUGH = Symbol('PASS_THROUGH'); +export type PassThrough = typeof PASS_THROUGH; + +export const UNHANDLED = Symbol('UNHANDLED'); +export type Unhandled = typeof UNHANDLED; diff --git a/packages/dbml-parse/src/core/analyzer/analyzer.ts b/packages/dbml-parse/src/core/analyzer/analyzer.ts deleted file mode 100644 index 442c2053f..000000000 --- a/packages/dbml-parse/src/core/analyzer/analyzer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Validator from '@/core/analyzer/validator/validator'; -import Binder from '@/core/analyzer/binder/binder'; -import { ProgramNode } from '@/core/parser/nodes'; -import Report from '@/core/report'; -import { NodeSymbolIdGenerator } from '@/core/analyzer/symbol/symbols'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; - -export default class Analyzer { - private ast: ProgramNode; - private symbolFactory: SymbolFactory; - - constructor (ast: ProgramNode, symbolIdGenerator: NodeSymbolIdGenerator) { - this.ast = ast; - this.symbolFactory = new SymbolFactory(symbolIdGenerator); - } - - // Analyzing: Invoking both the validator and binder - analyze (): Report { - const validator = new Validator(this.ast, this.symbolFactory); - - return validator.validate().chain((program) => { - const binder = new Binder(program, this.symbolFactory); - - return binder.resolve(); - }); - } - - // For invoking the validator only - validate (): Report { - const validator = new Validator(this.ast, this.symbolFactory); - - return validator.validate().chain((program) => new Report(program, [])); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/binder.ts b/packages/dbml-parse/src/core/analyzer/binder/binder.ts deleted file mode 100644 index 0d415877a..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/binder.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CompileError } from '@/core/errors'; -import { ElementDeclarationNode, ProgramNode } from '@/core/parser/nodes'; -import { pickBinder } from '@/core/analyzer/binder/utils'; -import Report from '@/core/report'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { getElementKind } from '@/core/analyzer/utils'; -import { ElementKind } from '@/core/analyzer/types'; -import TableBinder from './elementBinder/table'; - -export default class Binder { - private ast: ProgramNode; - - private symbolFactory: SymbolFactory; - - constructor (ast: ProgramNode, symbolFactory: SymbolFactory) { - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - private resolvePartialInjections (): CompileError[] { - return this.ast.body.filter((e) => getElementKind(e).unwrap_or('') === ElementKind.Table).flatMap((t) => { - const binder = new TableBinder(t as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - return binder.resolvePartialInjections(); - }); - } - - resolve (): Report { - const errors: CompileError[] = []; - // Must call this before binding - errors.push(...this.resolvePartialInjections()); - - for (const element of this.ast.body) { - if (element.type) { - const _Binder = pickBinder(element as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(element as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - errors.push(...binder.bind()); - } - } - - return new Report(this.ast, errors); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/checks.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/checks.ts deleted file mode 100644 index 584534af4..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/checks.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ElementDeclarationNode, ProgramNode, SyntaxToken } from '../../../..'; -import { CompileError } from '../../../errors'; -import SymbolFactory from '../../symbol/factory'; -import { ElementBinder } from '../types'; - -export default class ChecksBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - return []; - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/custom.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/custom.ts deleted file mode 100644 index 0e6847977..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/custom.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { CompileError } from '../../../errors'; -import { ElementBinder } from '../types'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ProgramNode, -} from '../../../parser/nodes'; -import { SyntaxToken } from '../../../lexer/tokens'; -import { pickBinder } from '../utils'; -import SymbolFactory from '../../symbol/factory'; - -export default class CustomBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - return this.bindBody(this.declarationNode.body); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return []; - } - - const subs = body.body.filter((e) => e instanceof ElementDeclarationNode); - - return this.bindSubElements(subs as ElementDeclarationNode[]); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/enum.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/enum.ts deleted file mode 100644 index 1cce36678..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/enum.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { CompileError } from '../../../errors'; -import { ElementBinder } from '../types'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ProgramNode, -} from '../../../parser/nodes'; -import { SyntaxToken } from '../../../lexer/tokens'; -import { pickBinder } from '../utils'; -import SymbolFactory from '../../symbol/factory'; - -export default class EnumBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - if (!(this.declarationNode.body instanceof BlockExpressionNode)) { - return []; - } - - return this.bindBody(this.declarationNode.body); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return []; - } - - const subs = body.body.filter((e) => e instanceof FunctionApplicationNode); - - return this.bindSubElements(subs as ElementDeclarationNode[]); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/indexes.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/indexes.ts deleted file mode 100644 index 73f13c4c7..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/indexes.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { partition } from 'lodash-es'; -import { - BlockExpressionNode, - ElementDeclarationNode, - FunctionApplicationNode, - ProgramNode, -} from '../../../parser/nodes'; -import { ElementBinder } from '../types'; -import { SyntaxToken } from '../../../lexer/tokens'; -import { CompileError, CompileErrorCode } from '../../../errors'; -import { pickBinder, scanNonListNodeForBinding } from '../utils'; -import { destructureComplexVariable, extractVarNameFromPrimaryVariable, getElementKind } from '../../utils'; -import { ElementKind } from '../../types'; -import { createColumnSymbolIndex } from '../../symbol/symbolIndex'; -import SymbolFactory from '../../symbol/factory'; - -export default class IndexesBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - if (!(this.declarationNode.parent instanceof ElementDeclarationNode) || getElementKind(this.declarationNode.parent).unwrap_or(undefined) !== ElementKind.Table) { - return []; - } - - if (!(this.declarationNode.body instanceof BlockExpressionNode)) { - return []; - } - - return this.bindBody(this.declarationNode.body); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.bindFields([body]); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - - return [...this.bindFields(fields as FunctionApplicationNode[]), ...this.bindSubElements(subs as ElementDeclarationNode[])]; - } - - private bindFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => { - if (!field.callee) { - return []; - } - const ownerTableName = destructureComplexVariable( - (this.declarationNode.parent! as ElementDeclarationNode).name, - ).map( - (fragments) => fragments.join('.'), - ).unwrap_or(''); - const ownerTableSymbolTable = this.declarationNode.parent!.symbol!.symbolTable!; - - const args = [field.callee, ...field.args]; - const bindees = args.flatMap(scanNonListNodeForBinding) - .flatMap((bindee) => { - if (bindee.variables.length + bindee.tupleElements.length > 1) { - return []; - } - if (bindee.variables.length) { - return bindee.variables[0]; - } - - return bindee.tupleElements; - }); - - return bindees.flatMap((bindee) => { - const columnName = extractVarNameFromPrimaryVariable(bindee).unwrap_or(undefined); - if (columnName === undefined) return []; - const columnIndex = createColumnSymbolIndex(columnName); - const column = ownerTableSymbolTable.get(columnIndex); - if (!column) { - return new CompileError(CompileErrorCode.BINDING_ERROR, `No column named '${columnName}' inside Table '${ownerTableName}'`, bindee); - } - bindee.referee = column; - column.references.push(bindee); - - return []; - }); - }); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/note.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/note.ts deleted file mode 100644 index b967a06c3..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/note.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { CompileError } from '../../../errors'; -import { ElementBinder } from '../types'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ProgramNode, -} from '../../../parser/nodes'; -import { SyntaxToken } from '../../../lexer/tokens'; -import { pickBinder } from '../utils'; -import SymbolFactory from '../../symbol/factory'; - -export default class NoteBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - return this.bindBody(this.declarationNode.body); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return []; - } - - const subs = body.body.filter((e) => e instanceof ElementDeclarationNode); - - return this.bindSubElements(subs as ElementDeclarationNode[]); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/project.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/project.ts deleted file mode 100644 index b7b4c6bbf..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/project.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SyntaxToken } from '../../../lexer/tokens'; -import { ElementBinder } from '../types'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ProgramNode, -} from '../../../parser/nodes'; -import { CompileError } from '../../../errors'; -import { pickBinder } from '../utils'; -import SymbolFactory from '../../symbol/factory'; - -export default class ProjectBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - if (!(this.declarationNode.body instanceof BlockExpressionNode)) { - return []; - } - - return this.bindBody(this.declarationNode.body); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return []; - } - - const subs = body.body.filter((e) => e instanceof ElementDeclarationNode); - - return this.bindSubElements(subs as ElementDeclarationNode[]); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/records.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/records.ts deleted file mode 100644 index 26a09fbf0..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/records.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { SyntaxToken } from '../../../lexer/tokens'; -import { ElementBinder } from '../types'; -import { - BlockExpressionNode, CommaExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ProgramNode, SyntaxNode, -} from '../../../parser/nodes'; -import { CompileError, CompileErrorCode } from '../../../errors'; -import { lookupAndBindInScope, pickBinder, scanNonListNodeForBinding } from '../utils'; -import SymbolFactory from '../../symbol/factory'; -import { - destructureCallExpression, - extractVarNameFromPrimaryVariable, - getElementKind, -} from '../../utils'; -import { createColumnSymbolIndex, SymbolKind } from '../../symbol/symbolIndex'; -import { ElementKind } from '../../types'; -import { isTupleOfVariables } from '../../validator/utils'; -import { NodeSymbol } from '../../symbol/symbols'; -import { getElementNameString } from '@/core/parser/utils'; - -export default class RecordsBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - // A mapping from bound column symbols to the referencing primary expressions nodes of column - // Example: Records (col1, col2) -> Map symbol of `col1` to the `col1` in `Records (col1, col2)`` - private boundColumns: Map; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - this.boundColumns = new Map(); - } - - bind (): CompileError[] { - const errors: CompileError[] = []; - - if (this.declarationNode.name) { - errors.push(...this.bindRecordsName(this.declarationNode.name)); - } - - if (this.declarationNode.body instanceof BlockExpressionNode) { - errors.push(...this.bindBody(this.declarationNode.body)); - } - - return errors; - } - - private bindRecordsName (nameNode: SyntaxNode): CompileError[] { - const parent = this.declarationNode.parent; - const isTopLevel = parent instanceof ProgramNode; - - return isTopLevel - ? this.bindTopLevelName(nameNode) - : this.bindInsideTableName(nameNode); - } - - // At top-level - bind table and column references: - // records users(id, name) { } // binds: Table[users], Column[id], Column[name] - // records myschema.users(id, name) { } // binds: Schema[myschema], Table[users], Column[id], Column[name] - private bindTopLevelName (nameNode: SyntaxNode): CompileError[] { - const fragments = destructureCallExpression(nameNode).unwrap_or(undefined); - if (!fragments) { - return []; - } - - const tableBindee = fragments.variables.pop(); - const schemaBindees = fragments.variables; - - if (!tableBindee) { - return []; - } - - const tableErrors = lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: tableBindee, kind: SymbolKind.Table }, - ]); - - if (tableErrors.length > 0) { - return tableErrors; - } - - const tableSymbol = tableBindee.referee; - if (!tableSymbol?.symbolTable) { - return []; - } - - const tableName = getElementNameString(tableBindee.referee?.declaration).unwrap_or(''); - - const errors: CompileError[] = []; - for (const columnBindee of fragments.args) { - const columnName = extractVarNameFromPrimaryVariable(columnBindee).unwrap_or(''); - const columnIndex = createColumnSymbolIndex(columnName); - const columnSymbol = tableSymbol.symbolTable.get(columnIndex); - - if (!columnSymbol) { - errors.push(new CompileError( - CompileErrorCode.BINDING_ERROR, - `Column '${columnName}' does not exist in Table '${tableName}'`, - columnBindee, - )); - continue; - } - columnBindee.referee = columnSymbol; - columnSymbol.references.push(columnBindee); - - const originalBindee = this.boundColumns.get(columnSymbol); - if (originalBindee) { - errors.push(new CompileError( - CompileErrorCode.DUPLICATE_COLUMN_REFERENCES_IN_RECORDS, - `Column '${columnName}' is referenced more than once in a Records for Table '${tableName}'`, - originalBindee, - )); - errors.push(new CompileError( - CompileErrorCode.DUPLICATE_COLUMN_REFERENCES_IN_RECORDS, - `Column '${columnName}' is referenced more than once in a Records for Table '${tableName}'`, - columnBindee, - )); - } - this.boundColumns.set(columnSymbol, columnBindee); - } - - return errors; - } - - // Inside a table - bind column references to parent table: - // table users { records (id, name) { } } // binds: Column[id], Column[name] from parent table - // table users { records { } } // no columns to bind - private bindInsideTableName (nameNode: SyntaxNode): CompileError[] { - const parent = this.declarationNode.parent; - if (!(parent instanceof ElementDeclarationNode)) { - return []; - } - - const elementKind = getElementKind(parent).unwrap_or(undefined); - if (elementKind !== ElementKind.Table) { - return []; - } - - const tableSymbolTable = parent.symbol?.symbolTable; - if (!tableSymbolTable) { - return []; - } - - if (!isTupleOfVariables(nameNode)) { - return []; - } - - const tableName = getElementNameString(parent).unwrap_or(''); - - const errors: CompileError[] = []; - for (const columnBindee of nameNode.elementList) { - const columnName = extractVarNameFromPrimaryVariable(columnBindee).unwrap_or(''); - const columnIndex = createColumnSymbolIndex(columnName); - const columnSymbol = tableSymbolTable.get(columnIndex); - - if (!columnSymbol) { - errors.push(new CompileError( - CompileErrorCode.BINDING_ERROR, - `Column '${columnName}' does not exist in Table '${tableName}'`, - columnBindee, - )); - continue; - } - - columnBindee.referee = columnSymbol; - columnSymbol.references.push(columnBindee); - } - - return errors; - } - - // Bind enum field references in data rows. - // Example data rows with enum references: - // 1, status.active, 'hello' // binds: Enum[status], EnumField[active] - // myschema.status.pending, 42 // binds: Schema[myschema], Enum[status], EnumField[pending] - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.bindDataRow(body); - } - - const functions = body.body.filter((e) => e instanceof FunctionApplicationNode); - const subs = body.body.filter((e) => e instanceof ElementDeclarationNode); - - return [ - ...this.bindDataRows(functions as FunctionApplicationNode[]), - ...this.bindSubElements(subs as ElementDeclarationNode[]), - ]; - } - - private bindDataRows (rows: FunctionApplicationNode[]): CompileError[] { - return rows.flatMap((row) => this.bindDataRow(row)); - } - - // Bind a single data row. Structure: - // row.callee = CommaExpressionNode (e.g., 1, status.active, 'hello') or single value - // row.args = [] (empty) - private bindDataRow (row: FunctionApplicationNode): CompileError[] { - if (!row.callee) { - return []; - } - - const values = row.callee instanceof CommaExpressionNode - ? row.callee.elementList - : [row.callee]; - - const bindees = values.flatMap(scanNonListNodeForBinding); - - return bindees.flatMap((bindee) => { - const enumFieldBindee = bindee.variables.pop(); - const enumBindee = bindee.variables.pop(); - - if (!enumFieldBindee || !enumBindee) { - return []; - } - - const schemaBindees = bindee.variables; - - return lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: enumBindee, kind: SymbolKind.Enum }, - { node: enumFieldBindee, kind: SymbolKind.EnumField }, - ]); - }); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/ref.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/ref.ts deleted file mode 100644 index b45a0f876..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/ref.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { partition } from 'lodash-es'; -import { - BlockExpressionNode, - ElementDeclarationNode, - FunctionApplicationNode, - ProgramNode, -} from '../../../parser/nodes'; -import { ElementBinder } from '../types'; -import { SyntaxToken } from '../../../lexer/tokens'; -import { CompileError } from '../../../errors'; -import { lookupAndBindInScope, pickBinder, scanNonListNodeForBinding } from '../utils'; -import { getElementKind } from '../../utils'; -import { ElementKind } from '../../types'; -import { SymbolKind } from '../../symbol/symbolIndex'; -import SymbolFactory from '../../symbol/factory'; - -export default class RefBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - if (!(this.declarationNode.parent instanceof ProgramNode) && getElementKind(this.declarationNode.parent).unwrap_or(undefined) !== ElementKind.Project) { - return []; - } - - return this.bindBody(this.declarationNode.body); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.bindFields([body]); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - - return [...this.bindFields(fields as FunctionApplicationNode[]), ...this.bindSubElements(subs as ElementDeclarationNode[])]; - } - - private bindFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => { - if (!field.callee) { - return []; - } - - const args = [field.callee, ...field.args]; - const bindees = args.flatMap(scanNonListNodeForBinding); - - return bindees.flatMap((bindee) => { - let columnBindees = bindee.tupleElements.length ? bindee.tupleElements : bindee.variables.pop(); - const tableBindee = bindee.variables.pop(); - if (!columnBindees || !tableBindee) { - return []; - } - if (!Array.isArray(columnBindees)) { - columnBindees = [columnBindees]; - } - - const schemaBindees = bindee.variables; - - return columnBindees.flatMap((columnBindee) => lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: tableBindee, kind: SymbolKind.Table }, - { node: columnBindee, kind: SymbolKind.Column }, - ])); - }); - }); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/table.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/table.ts deleted file mode 100644 index f43c96d06..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/table.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { last, partition } from 'lodash-es'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, PrefixExpressionNode, ProgramNode, SyntaxNode, -} from '../../../parser/nodes'; -import { ElementBinder } from '../types'; -import { SyntaxToken } from '../../../lexer/tokens'; -import { CompileError } from '../../../errors'; -import { lookupAndBindInScope, pickBinder, scanNonListNodeForBinding } from '../utils'; -import { aggregateSettingList, isValidPartialInjection } from '../../validator/utils'; -import { SymbolKind, createColumnSymbolIndex } from '../../symbol/symbolIndex'; -import { destructureComplexVariableTuple, extractVariableFromExpression } from '../../utils'; -import { TablePartialInjectedColumnSymbol, TablePartialSymbol } from '../../symbol/symbols'; -import SymbolFactory from '../../symbol/factory'; -import { isExpressionAQuotedString, isExpressionAVariableNode } from '../../../parser/utils'; -import { KEYWORDS_OF_DEFAULT_SETTING } from '@/constants'; - -export default class TableBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - if (!(this.declarationNode.body instanceof BlockExpressionNode)) { - return []; - } - - return this.bindBody(this.declarationNode.body); - } - - // Must call this before any bind methods of any binder classes - resolvePartialInjections (): CompileError[] { - const { body } = this.declarationNode; - const members = !body - ? [] - : body instanceof BlockExpressionNode - ? body.body - : [body]; - // Prioritize the later injections - return members - .filter((i) => i instanceof FunctionApplicationNode && isValidPartialInjection(i.callee)) - .reverse() // Warning: `reverse` mutates, but it's safe because we're working on a filtered array - .flatMap((i) => { - const fragments = destructureComplexVariableTuple(((i as FunctionApplicationNode).callee as PrefixExpressionNode).expression).unwrap_or(undefined); - if (!fragments) return []; - const tablePartialBindee = fragments.variables.pop(); - const schemaBindees = fragments.variables; - - if (!tablePartialBindee) { - return []; - } - - const errors = lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: tablePartialBindee, kind: SymbolKind.TablePartial }, - ]); - if (errors.length) return errors; - tablePartialBindee.referee?.symbolTable?.forEach((value) => { - const columnName = extractVariableFromExpression((value.declaration as FunctionApplicationNode).callee).unwrap_or(undefined); - if (columnName === undefined) return; - const injectedColumnSymbol = this.symbolFactory.create( - TablePartialInjectedColumnSymbol, - { declaration: i, tablePartialSymbol: tablePartialBindee.referee as TablePartialSymbol }, - ); - const columnSymbolId = createColumnSymbolIndex(columnName); - const symbolTable = this.declarationNode.symbol?.symbolTable; - if (symbolTable?.has(columnSymbolId)) return; - symbolTable?.set(columnSymbolId, injectedColumnSymbol); - }); - return []; - }); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.bindFields([body]); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - - return [ - ...this.bindFields(fields as FunctionApplicationNode[]), - ...this.bindSubElements(subs as ElementDeclarationNode[]), - ]; - } - - private bindFields (fields: FunctionApplicationNode[]): CompileError[] { - const columns = fields.filter((f) => !isValidPartialInjection(f.callee)); - - const bindColumns = (cs: FunctionApplicationNode[]): CompileError[] => { - return cs.flatMap((c) => { - if (!c.callee) { - return []; - } - - const errors: CompileError[] = []; - - const args = [c.callee, ...c.args]; - if (last(args) instanceof ListExpressionNode) { - const listExpression = last(args) as ListExpressionNode; - const settingsMap = aggregateSettingList(listExpression).getValue(); - - errors.push(...(settingsMap.ref?.flatMap((ref) => (ref.value ? this.bindInlineRef(ref.value) : [])) || [])); - errors.push(...(settingsMap.default?.flatMap((def) => (def.value ? this.tryToBindEnumFieldRef(def.value) : [])) || [])); - args.pop(); - } - - if (!args[1]) { - return errors; - } - this.tryToBindColumnType(args[1]); - - return errors; - }); - }; - return bindColumns(columns); - } - - private tryToBindColumnType (typeNode: SyntaxNode) { - const fragments = destructureComplexVariableTuple(typeNode).unwrap_or(undefined); - if (!fragments) { - return; - } - - const enumBindee = fragments.variables.pop(); - const schemaBindees = fragments.variables; - - if (!enumBindee) { - return; - } - - lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: enumBindee, kind: SymbolKind.Enum }, - ]); - } - - // Bind enum field references in default values (e.g., order_status.pending) - private tryToBindEnumFieldRef (defaultValue: SyntaxNode): CompileError[] { - // Skip quoted strings (e.g., [default: "hello"] or [default: `hello`]) - if (isExpressionAQuotedString(defaultValue)) { - return []; - } - - // Skip keywords (null, true, false) - if (isExpressionAVariableNode(defaultValue)) { - const varName = defaultValue.expression.variable?.value?.toLowerCase(); - if (varName && KEYWORDS_OF_DEFAULT_SETTING.includes(varName)) { - return []; - } - } - - const fragments = destructureComplexVariableTuple(defaultValue).unwrap_or(undefined); - if (!fragments) { - return []; - } - - const enumFieldBindee = fragments.variables.pop(); - const enumBindee = fragments.variables.pop(); - - if (!enumFieldBindee || !enumBindee) { - return []; - } - - const schemaBindees = fragments.variables; - - return lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: enumBindee, kind: SymbolKind.Enum }, - { node: enumFieldBindee, kind: SymbolKind.EnumField }, - ]); - } - - private bindInlineRef (ref: SyntaxNode): CompileError[] { - const bindees = scanNonListNodeForBinding(ref); - - return bindees.flatMap((bindee) => { - const columnBindee = bindee.variables.pop(); - const tableBindee = bindee.variables.pop(); - if (!columnBindee) { - return []; - } - const schemaBindees = bindee.variables; - - return tableBindee - ? lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: tableBindee, kind: SymbolKind.Table }, - { node: columnBindee, kind: SymbolKind.Column }, - ]) - : lookupAndBindInScope(this.declarationNode, [ - { node: columnBindee, kind: SymbolKind.Column }, - ]); - }); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/tableGroup.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/tableGroup.ts deleted file mode 100644 index 45caa82a0..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/tableGroup.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { partition } from 'lodash-es'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ProgramNode, -} from '../../../parser/nodes'; -import { ElementBinder } from '../types'; -import { SyntaxToken } from '../../../lexer/tokens'; -import { CompileError } from '../../../errors'; -import { lookupAndBindInScope, pickBinder, scanNonListNodeForBinding } from '../utils'; -import { SymbolKind } from '../../symbol/symbolIndex'; -import SymbolFactory from '../../symbol/factory'; - -export default class TableGroupBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - if (!(this.declarationNode.body instanceof BlockExpressionNode)) { - return []; - } - - return this.bindBody(this.declarationNode.body); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.bindFields([body]); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - - return [...this.bindFields(fields as FunctionApplicationNode[]), ...this.bindSubElements(subs as ElementDeclarationNode[])]; - } - - private bindFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => { - if (!field.callee) { - return []; - } - - const args = [field.callee, ...field.args]; - const bindees = args.flatMap(scanNonListNodeForBinding); - - return bindees.flatMap((bindee) => { - const tableBindee = bindee.variables.pop(); - if (!tableBindee) { - return []; - } - const schemaBindees = bindee.variables; - - return lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: tableBindee, kind: SymbolKind.Table }, - ]); - }); - }); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/tablePartial.ts b/packages/dbml-parse/src/core/analyzer/binder/elementBinder/tablePartial.ts deleted file mode 100644 index 55370a6e6..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/elementBinder/tablePartial.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { last, partition } from 'lodash-es'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, ProgramNode, SyntaxNode, -} from '../../../parser/nodes'; -import { SyntaxToken } from '../../../lexer/tokens'; -import { ElementBinder } from '../types'; -import { CompileError } from '../../../errors'; -import { aggregateSettingList } from '../../validator/utils'; -import { destructureComplexVariableTuple } from '../../utils'; -import { lookupAndBindInScope, pickBinder, scanNonListNodeForBinding } from '../utils'; -import { SymbolKind } from '../../symbol/symbolIndex'; -import SymbolFactory from '../../symbol/factory'; - -export default class TablePartialBinder implements ElementBinder { - private symbolFactory: SymbolFactory; - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private ast: ProgramNode; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, ast: ProgramNode, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.ast = ast; - this.symbolFactory = symbolFactory; - } - - bind (): CompileError[] { - if (!(this.declarationNode.body instanceof BlockExpressionNode)) { - return []; - } - - return this.bindBody(this.declarationNode.body); - } - - private bindBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.bindFields([body]); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - - return [...this.bindFields(fields as FunctionApplicationNode[]), ...this.bindSubElements(subs as ElementDeclarationNode[])]; - } - - private bindFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => { - if (!field.callee) { - return []; - } - - const errors: CompileError[] = []; - - const args = [field.callee, ...field.args]; - if (last(args) instanceof ListExpressionNode) { - const listExpression = last(args) as ListExpressionNode; - const settingsMap = aggregateSettingList(listExpression).getValue(); - - errors.push(...(settingsMap.ref?.flatMap((ref) => (ref.value ? this.bindInlineRef(ref.value) : [])) || [])); - args.pop(); - } - - if (!args[1]) { - return errors; - } - this.tryToBindColumnType(args[1]); - - return errors; - }); - } - - private tryToBindColumnType (typeNode: SyntaxNode) { - const fragments = destructureComplexVariableTuple(typeNode).unwrap_or(undefined); - if (!fragments) { - return; - } - - const enumBindee = fragments.variables.pop(); - const schemaBindees = fragments.variables; - - if (!enumBindee) { - return; - } - - lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: enumBindee, kind: SymbolKind.Enum }, - ]); - } - - private bindInlineRef (ref: SyntaxNode): CompileError[] { - const bindees = scanNonListNodeForBinding(ref); - - return bindees.flatMap((bindee) => { - const columnBindee = bindee.variables.pop(); - const tableBindee = bindee.variables.pop(); - if (!columnBindee) { - return []; - } - const schemaBindees = bindee.variables; - - return tableBindee - ? lookupAndBindInScope(this.ast, [ - ...schemaBindees.map((b) => ({ node: b, kind: SymbolKind.Schema })), - { node: tableBindee, kind: SymbolKind.Table }, - { node: columnBindee, kind: SymbolKind.Column }, - ]) - : lookupAndBindInScope(this.declarationNode, [ - { node: columnBindee, kind: SymbolKind.Column }, - ]); - }); - } - - private bindSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - if (!sub.type) { - return []; - } - const _Binder = pickBinder(sub as ElementDeclarationNode & { type: SyntaxToken }); - const binder = new _Binder(sub as ElementDeclarationNode & { type: SyntaxToken }, this.ast, this.symbolFactory); - - return binder.bind(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/types.ts b/packages/dbml-parse/src/core/analyzer/binder/types.ts deleted file mode 100644 index 6b3a2aff8..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CompileError } from '../../errors'; - -export interface ElementBinder { - bind(): CompileError[]; -} diff --git a/packages/dbml-parse/src/core/analyzer/binder/utils.ts b/packages/dbml-parse/src/core/analyzer/binder/utils.ts deleted file mode 100644 index 7157c3ed3..000000000 --- a/packages/dbml-parse/src/core/analyzer/binder/utils.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { SyntaxToken } from '@/core/lexer/tokens'; -import { ElementDeclarationNode, InfixExpressionNode, PostfixExpressionNode, PrefixExpressionNode, PrimaryExpressionNode, ProgramNode, SyntaxNode, TupleExpressionNode, VariableNode } from '@/core/parser/nodes'; -import { ElementKind } from '@/core/analyzer/types'; -import ChecksBinder from './elementBinder/checks'; -import CustomBinder from './elementBinder/custom'; -import EnumBinder from './elementBinder/enum'; -import IndexesBinder from './elementBinder/indexes'; -import NoteBinder from './elementBinder/note'; -import ProjectBinder from './elementBinder/project'; -import RefBinder from './elementBinder/ref'; -import TableBinder from './elementBinder/table'; -import TableGroupBinder from './elementBinder/tableGroup'; -import TablePartialBinder from './elementBinder/tablePartial'; -import { destructureComplexVariableTuple, extractVarNameFromPrimaryVariable } from '@/core/analyzer/utils'; -import { SymbolKind, createNodeSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; -import { getSymbolKind } from '@/core/analyzer/symbol/utils'; -import { getElementNameString, isExpressionAVariableNode } from '@/core/parser/utils'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { DEFAULT_SCHEMA_NAME } from '@/constants'; -import RecordsBinder from './elementBinder/records'; - -export function pickBinder (element: ElementDeclarationNode & { type: SyntaxToken }) { - switch (element.type.value.toLowerCase() as ElementKind) { - case ElementKind.Enum: - return EnumBinder; - case ElementKind.Table: - return TableBinder; - case ElementKind.TableGroup: - return TableGroupBinder; - case ElementKind.Project: - return ProjectBinder; - case ElementKind.Ref: - return RefBinder; - case ElementKind.Note: - return NoteBinder; - case ElementKind.Indexes: - return IndexesBinder; - case ElementKind.TablePartial: - return TablePartialBinder; - case ElementKind.Check: - return ChecksBinder; - case ElementKind.Records: - return RecordsBinder; - default: - return CustomBinder; - } -} - -// Scan for variable node and member access expression in the node except ListExpressionNode -export function scanNonListNodeForBinding (node?: SyntaxNode): -{ variables: (PrimaryExpressionNode & { expression: VariableNode })[]; tupleElements: (PrimaryExpressionNode & { expression: VariableNode })[] }[] { - if (!node) { - return []; - } - - if (isExpressionAVariableNode(node)) { - return [{ variables: [node], tupleElements: [] }]; - } - - if (node instanceof InfixExpressionNode) { - const fragments = destructureComplexVariableTuple(node).unwrap_or(undefined); - if (!fragments) { - return [...scanNonListNodeForBinding(node.leftExpression), ...scanNonListNodeForBinding(node.rightExpression)]; - } - - return [fragments]; - } - - if (node instanceof PrefixExpressionNode) { - return scanNonListNodeForBinding(node.expression); - } - - if (node instanceof PostfixExpressionNode) { - return scanNonListNodeForBinding(node.expression); - } - - if (node instanceof TupleExpressionNode) { - const fragments = destructureComplexVariableTuple(node).unwrap_or(undefined); - if (!fragments) { - // Tuple elements are not simple variables (e.g., member access expressions like table.column) - // Recurse into each element - return node.elementList.flatMap(scanNonListNodeForBinding); - } - return [fragments]; - } - - // The other cases are not supported as practically they shouldn't arise - return []; -} - -export function lookupAndBindInScope ( - initialScope: ElementDeclarationNode | ProgramNode, - symbolInfos: { node: PrimaryExpressionNode & { expression: VariableNode }; kind: SymbolKind }[], -): CompileError[] { - if (!initialScope.symbol?.symbolTable) { - throw new Error('lookupAndBindInScope should only be called with initial scope having a symbol table'); - } - - let curSymbolTable = initialScope.symbol.symbolTable; - let curKind = getSymbolKind(initialScope.symbol); - let curName = initialScope instanceof ElementDeclarationNode ? getElementNameString(initialScope).unwrap_or('') : DEFAULT_SCHEMA_NAME; - - if (initialScope instanceof ProgramNode && symbolInfos.length) { - const { node, kind } = symbolInfos[0]; - const name = extractVarNameFromPrimaryVariable(node).unwrap_or(''); - if (name === DEFAULT_SCHEMA_NAME && kind === SymbolKind.Schema) { - symbolInfos.shift(); - } - } - - for (const curSymbolInfo of symbolInfos) { - const { node, kind } = curSymbolInfo; - const name = extractVarNameFromPrimaryVariable(node).unwrap_or(''); - const index = createNodeSymbolIndex(name, kind); - const symbol = curSymbolTable.get(index); - - if (!symbol) { - return [new CompileError(CompileErrorCode.BINDING_ERROR, `${kind} '${name}' does not exist in ${curName === undefined ? 'global scope' : `${curKind} '${curName}'`}`, node)]; - } - node.referee = symbol; - symbol.references.push(node); - - curName = name; - curKind = kind; - if (!symbol.symbolTable) { - return []; - } - curSymbolTable = symbol.symbolTable; - } - - return []; -} diff --git a/packages/dbml-parse/src/core/analyzer/symbol/factory.ts b/packages/dbml-parse/src/core/analyzer/symbol/factory.ts deleted file mode 100644 index ab88eb53f..000000000 --- a/packages/dbml-parse/src/core/analyzer/symbol/factory.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NodeSymbol, NodeSymbolId, NodeSymbolIdGenerator } from './symbols'; - -export default class SymbolFactory { - private generator: NodeSymbolIdGenerator; - - constructor (generator: NodeSymbolIdGenerator) { - this.generator = generator; - } - - create(Type: { new (args: A, id: NodeSymbolId): T }, args: A): T { - return new Type(args, this.generator.nextId()); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/symbol/symbolIndex.ts b/packages/dbml-parse/src/core/analyzer/symbol/symbolIndex.ts deleted file mode 100644 index 35f191e16..000000000 --- a/packages/dbml-parse/src/core/analyzer/symbol/symbolIndex.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { DEFAULT_SCHEMA_NAME } from '@/constants'; -import { None, Option, Some } from '@/core/option'; - -// Used to index a symbol table to obtain a symbol -export type NodeSymbolIndex = string; -export enum SymbolKind { - Schema = 'Schema', - Table = 'Table', - Column = 'Column', - TableGroup = 'TableGroup', - TableGroupField = 'TableGroup field', - Enum = 'Enum', - EnumField = 'Enum field', - Note = 'Note', - TablePartial = 'TablePartial', - PartialInjection = 'PartialInjection', -} - -export function createSchemaSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.Schema}:${key}`; -} - -export function createTableSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.Table}:${key}`; -} - -export function createColumnSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.Column}:${key}`; -} - -export function createEnumSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.Enum}:${key}`; -} - -export function createEnumFieldSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.EnumField}:${key}`; -} - -export function createTableGroupSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.TableGroup}:${key}`; -} - -export function createTableGroupFieldSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.TableGroupField}:${key}`; -} - -export function createStickyNoteSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.Note}:${key}`; -} - -export function createTablePartialSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.TablePartial}:${key}`; -} - -export function createPartialInjectionSymbolIndex (key: string): NodeSymbolIndex { - return `${SymbolKind.PartialInjection}:${key}`; -} - -export function createNodeSymbolIndex (key: string, symbolKind: SymbolKind): NodeSymbolIndex { - switch (symbolKind) { - case SymbolKind.Column: - return createColumnSymbolIndex(key); - case SymbolKind.Enum: - return createEnumSymbolIndex(key); - case SymbolKind.EnumField: - return createEnumFieldSymbolIndex(key); - case SymbolKind.Schema: - return createSchemaSymbolIndex(key); - case SymbolKind.Table: - return createTableSymbolIndex(key); - case SymbolKind.TableGroup: - return createTableGroupSymbolIndex(key); - case SymbolKind.TableGroupField: - return createTableGroupFieldSymbolIndex(key); - case SymbolKind.TablePartial: - return createTablePartialSymbolIndex(key); - case SymbolKind.PartialInjection: - return createPartialInjectionSymbolIndex(key); - default: - throw new Error('Unreachable'); - } -} - -export function destructureIndex (id: NodeSymbolIndex): Option<{ name: string; kind: SymbolKind }> { - const [kind, name] = id.split(':'); - - return Object.values(SymbolKind).includes(kind as SymbolKind) - ? new Some({ - name, - kind: kind as SymbolKind, - }) - : new None(); -} - -export function isPublicSchemaIndex (id: NodeSymbolIndex): boolean { - const res = destructureIndex(id).unwrap_or(undefined); - if (!res) { - return false; - } - const { kind, name } = res; - - return kind === 'Schema' && name === DEFAULT_SCHEMA_NAME; -} diff --git a/packages/dbml-parse/src/core/analyzer/symbol/symbolTable.ts b/packages/dbml-parse/src/core/analyzer/symbol/symbolTable.ts deleted file mode 100644 index 72d7e46a4..000000000 --- a/packages/dbml-parse/src/core/analyzer/symbol/symbolTable.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NodeSymbolIndex } from './symbolIndex'; -import { NodeSymbol } from './symbols'; - -export default class SymbolTable { - private table: Map; - - constructor () { - this.table = new Map(); - } - - has (id: NodeSymbolIndex): boolean { - return this.table.has(id); - } - - set (id: NodeSymbolIndex, value: NodeSymbol) { - this.table.set(id, value); - } - - get (id: NodeSymbolIndex): NodeSymbol | undefined; - get (id: NodeSymbolIndex, defaultValue: NodeSymbol): NodeSymbol; - get (id: NodeSymbolIndex, defaultValue?: NodeSymbol): NodeSymbol | undefined { - return ( - this.table.get(id) - || (defaultValue !== undefined && this.set(id, defaultValue)) - || defaultValue - ); - } - - entries (): IterableIterator<[NodeSymbolIndex, NodeSymbol]> { - return this.table.entries(); - } - - forEach (callback: (value: NodeSymbol, key: NodeSymbolIndex) => void) { - return this.table.forEach(callback); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/symbol/symbols.ts b/packages/dbml-parse/src/core/analyzer/symbol/symbols.ts deleted file mode 100644 index 8ba24822a..000000000 --- a/packages/dbml-parse/src/core/analyzer/symbol/symbols.ts +++ /dev/null @@ -1,201 +0,0 @@ -import SymbolTable from './symbolTable'; -import { SyntaxNode } from '@/core/parser/nodes'; - -export type NodeSymbolId = number; -export class NodeSymbolIdGenerator { - private id = 0; - - reset () { - this.id = 0; - } - - nextId (): NodeSymbolId { - return this.id++; - } -} - -// A Symbol contains metadata about an entity (Enum, Table, etc.) -// This does not include `name` as an entity may have multiple names (e.g alias) -export interface NodeSymbol { - id: NodeSymbolId; - symbolTable?: SymbolTable; - declaration?: SyntaxNode; - references: SyntaxNode[]; -} - -// A symbol for a schema, contains the schema's symbol table -export class SchemaSymbol implements NodeSymbol { - id: NodeSymbolId; - - symbolTable: SymbolTable; - - references: SyntaxNode[] = []; - - constructor ({ symbolTable }: { symbolTable: SymbolTable }, id: NodeSymbolId) { - this.id = id; - this.symbolTable = symbolTable; - } -} - -// A symbol for an enum, contains the enum's symbol table -// which is used to hold all the enum field symbols of the enum -export class EnumSymbol implements NodeSymbol { - id: NodeSymbolId; - - symbolTable: SymbolTable; - - declaration: SyntaxNode; - - references: SyntaxNode[] = []; - - constructor ( - { symbolTable, declaration }: { symbolTable: SymbolTable; declaration: SyntaxNode }, - id: NodeSymbolId, - ) { - this.id = id; - this.symbolTable = symbolTable; - this.declaration = declaration; - } -} - -// A symbol for an enum field -export class EnumFieldSymbol implements NodeSymbol { - id: NodeSymbolId; - - declaration: SyntaxNode; - - references: SyntaxNode[] = []; - - constructor ({ declaration }: { declaration: SyntaxNode }, id: NodeSymbolId) { - this.id = id; - this.declaration = declaration; - } -} - -// A symbol for a table, contains the table's symbol table -// which is used to hold all the column and table partial symbols of the table -export class TableSymbol implements NodeSymbol { - id: NodeSymbolId; - - symbolTable: SymbolTable; - - declaration: SyntaxNode; - - references: SyntaxNode[] = []; - - constructor ( - { symbolTable, declaration }: { symbolTable: SymbolTable; declaration: SyntaxNode }, - id: NodeSymbolId, - ) { - this.id = id; - this.symbolTable = symbolTable; - this.declaration = declaration; - } -} - -// A symbol for a column field -export class ColumnSymbol implements NodeSymbol { - id: NodeSymbolId; - - declaration: SyntaxNode; - - references: SyntaxNode[] = []; - - constructor ({ declaration }: { declaration: SyntaxNode }, id: NodeSymbolId) { - this.id = id; - this.declaration = declaration; - } -} - -// A symbol for a tablegroup, contains the symbol table for the tablegroup -// which is used to hold all the symbols of the table group fields -export class TableGroupSymbol implements NodeSymbol { - id: NodeSymbolId; - - symbolTable: SymbolTable; - - declaration: SyntaxNode; - - references: SyntaxNode[] = []; - - constructor ( - { symbolTable, declaration }: { symbolTable: SymbolTable; declaration: SyntaxNode }, - id: NodeSymbolId, - ) { - this.id = id; - this.symbolTable = symbolTable; - this.declaration = declaration; - } -} - -// A symbol for a tablegroup field -export class TableGroupFieldSymbol implements NodeSymbol { - id: NodeSymbolId; - - declaration: SyntaxNode; - - references: SyntaxNode[] = []; - - constructor ({ declaration }: { declaration: SyntaxNode }, id: NodeSymbolId) { - this.id = id; - this.declaration = declaration; - } -} - -// A symbol for a table partial, contains the table partial's symbol table -// which is used to hold all the column symbols of the table partial -export class TablePartialSymbol implements NodeSymbol { - id: NodeSymbolId; - - symbolTable: SymbolTable; - - declaration: SyntaxNode; - - references: SyntaxNode[] = []; - - constructor ( - { symbolTable, declaration }: { symbolTable: SymbolTable; declaration: SyntaxNode }, - id: NodeSymbolId, - ) { - this.id = id; - this.symbolTable = symbolTable; - this.declaration = declaration; - } -} - -// A member symbol for a Table injecting a TablePartial -export class PartialInjectionSymbol implements NodeSymbol { - id: NodeSymbolId; - - symbolTable: SymbolTable; - - declaration: SyntaxNode; - - references: SyntaxNode[] = []; - - constructor ( - { symbolTable, declaration }: { symbolTable: SymbolTable; declaration: SyntaxNode }, - id: NodeSymbolId, - ) { - this.id = id; - this.symbolTable = symbolTable; - this.declaration = declaration; - } -} - -// A symbol for a column field -export class TablePartialInjectedColumnSymbol implements NodeSymbol { - id: NodeSymbolId; - - declaration: SyntaxNode; - - tablePartialSymbol: TablePartialSymbol; - - references: SyntaxNode[] = []; - - constructor ({ declaration, tablePartialSymbol }: { declaration: SyntaxNode; tablePartialSymbol: TablePartialSymbol }, id: NodeSymbolId) { - this.id = id; - this.declaration = declaration; - this.tablePartialSymbol = tablePartialSymbol; - } -} diff --git a/packages/dbml-parse/src/core/analyzer/symbol/utils.ts b/packages/dbml-parse/src/core/analyzer/symbol/utils.ts deleted file mode 100644 index 6a958c23d..000000000 --- a/packages/dbml-parse/src/core/analyzer/symbol/utils.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - NodeSymbolIndex, - SymbolKind, - createColumnSymbolIndex, - createEnumFieldSymbolIndex, - createEnumSymbolIndex, - createPartialInjectionSymbolIndex, - createSchemaSymbolIndex, - createTableGroupFieldSymbolIndex, - createTableGroupSymbolIndex, - createTablePartialSymbolIndex, - createTableSymbolIndex, -} from './symbolIndex'; -import { - ColumnSymbol, - NodeSymbol, - TablePartialInjectedColumnSymbol, - SchemaSymbol, - TableGroupFieldSymbol, - TableGroupSymbol, - TableSymbol, - EnumSymbol, - EnumFieldSymbol, - TablePartialSymbol, - PartialInjectionSymbol, -} from './symbols'; - -// Given `name`, generate indexes with `name` and all possible kind -// e.g `Schema:name`, `Table:name`, etc. -export function generatePossibleIndexes (name: string): NodeSymbolIndex[] { - return [ - createSchemaSymbolIndex, - createTableSymbolIndex, - createEnumSymbolIndex, - createTableGroupSymbolIndex, - createColumnSymbolIndex, - createEnumFieldSymbolIndex, - createTableGroupFieldSymbolIndex, - createTablePartialSymbolIndex, - createPartialInjectionSymbolIndex, - ].map((f) => f(name)); -} - -export function getSymbolKind (symbol: NodeSymbol): SymbolKind { - if (symbol instanceof SchemaSymbol) { - return SymbolKind.Schema; - } - if (symbol instanceof TableSymbol) { - return SymbolKind.Table; - } - if (symbol instanceof ColumnSymbol) { - return SymbolKind.Column; - } - if (symbol instanceof EnumSymbol) { - return SymbolKind.Enum; - } - if (symbol instanceof EnumFieldSymbol) { - return SymbolKind.EnumField; - } - if (symbol instanceof TableGroupSymbol) { - return SymbolKind.TableGroup; - } - if (symbol instanceof TableGroupFieldSymbol) { - return SymbolKind.TableGroupField; - } - if (symbol instanceof TablePartialSymbol) { - return SymbolKind.TablePartial; - } - if (symbol instanceof TablePartialInjectedColumnSymbol) { - return SymbolKind.Column; - } - if (symbol instanceof PartialInjectionSymbol) { - return SymbolKind.PartialInjection; - } - throw new Error('No other possible symbol kind in getSymbolKind'); -} diff --git a/packages/dbml-parse/src/core/analyzer/utils.ts b/packages/dbml-parse/src/core/analyzer/utils.ts deleted file mode 100644 index 11a4762e4..000000000 --- a/packages/dbml-parse/src/core/analyzer/utils.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { last } from 'lodash-es'; -import { None, Option, Some } from '@/core/option'; -import { - ElementDeclarationNode, - FunctionExpressionNode, - InfixExpressionNode, - LiteralNode, - PrimaryExpressionNode, - ProgramNode, - SyntaxNode, - TupleExpressionNode, - VariableNode, - CallExpressionNode, -} from '@/core/parser/nodes'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; -import { isRelationshipOp, isTupleOfVariables } from '@/core/analyzer/validator/utils'; -import { NodeSymbolIndex, isPublicSchemaIndex } from '@/core/analyzer/symbol/symbolIndex'; -import { NodeSymbol } from '@/core/analyzer/symbol/symbols'; -import { - isAccessExpression, - isExpressionAQuotedString, - isExpressionAVariableNode, -} from '@/core/parser/utils'; -import { ElementKind } from '@/core/analyzer/types'; - -export function getElementKind (node?: ElementDeclarationNode): Option { - const kind = node?.type?.value.toLowerCase(); - switch (kind as ElementKind | undefined) { - case ElementKind.Enum: - case ElementKind.Table: - case ElementKind.Indexes: - case ElementKind.Note: - case ElementKind.Project: - case ElementKind.Ref: - case ElementKind.TableGroup: - case ElementKind.TablePartial: - case ElementKind.Check: - case ElementKind.Records: - return new Some(kind as ElementKind); - default: - return new None(); - } -} - -export function destructureMemberAccessExpression (node?: SyntaxNode): Option { - if (!node) return new None(); - - if (!isAccessExpression(node)) { - return new Some([node]); - } - - const fragments = destructureMemberAccessExpression(node.leftExpression).unwrap_or(undefined); - - if (!fragments) { - return new None(); - } - - fragments.push(node.rightExpression); - - return new Some(fragments); -} - -export function destructureComplexVariable (node?: SyntaxNode): Option { - if (node === undefined) { - return new None(); - } - - const fragments = destructureMemberAccessExpression(node).unwrap_or(undefined); - - if (!fragments) { - return new None(); - } - - const variables: string[] = []; - - for (const fragment of fragments) { - const variable = extractVariableFromExpression(fragment).unwrap_or(undefined); - if (typeof variable !== 'string') { - return new None(); - } - - variables.push(variable); - } - - return new Some(variables); -} - -export function destructureComplexVariableTuple ( - node?: SyntaxNode, -): Option<{ variables: (PrimaryExpressionNode & { expression: VariableNode })[]; tupleElements: (PrimaryExpressionNode & { expression: VariableNode })[] }> { - if (node === undefined) { - return new None(); - } - - const fragments = destructureMemberAccessExpression(node).unwrap_or(undefined); - - if (!fragments || fragments.length === 0) { - return new None(); - } - - let tupleElements: (PrimaryExpressionNode & { expression: VariableNode })[] = []; - - if (!isExpressionAVariableNode(last(fragments))) { - const topFragment = fragments.pop()!; - if (isTupleOfVariables(topFragment)) { - tupleElements = topFragment.elementList; - } else { - return new None(); - } - } - - const variables = fragments; - if (!variables.every(isExpressionAVariableNode)) { - return new None(); - } - - return new Some({ - variables, - tupleElements, - }); -} - -export function extractVariableFromExpression (node?: SyntaxNode): Option { - if (!isExpressionAVariableNode(node)) { - return new None(); - } - - return new Some(node.expression.variable.value); -} - -export function destructureIndexNode (node?: SyntaxNode): Option<{ - functional: FunctionExpressionNode[]; - nonFunctional: (PrimaryExpressionNode & { expression: VariableNode })[]; -}> { - if (isValidIndexName(node)) { - return node instanceof FunctionExpressionNode - ? new Some({ functional: [node], nonFunctional: [] }) - : new Some({ functional: [], nonFunctional: [node] }); - } - - if (node instanceof TupleExpressionNode && node.elementList.every(isValidIndexName)) { - const functionalIndexName = node.elementList.filter( - (e) => e instanceof FunctionExpressionNode, - ) as FunctionExpressionNode[]; - const nonfunctionalIndexName = node.elementList.filter(isExpressionAVariableNode); - - return new Some({ functional: functionalIndexName, nonFunctional: nonfunctionalIndexName }); - } - - return new None(); -} - -export function extractVarNameFromPrimaryVariable ( - node?: PrimaryExpressionNode & { expression: VariableNode }, -): Option { - const value = node?.expression.variable?.value; - - return value === undefined ? new None() : new Some(value); -} - -export function extractQuotedStringToken (value?: SyntaxNode): Option { - if (!isExpressionAQuotedString(value)) { - return new None(); - } - - if (value.expression instanceof VariableNode) { - return new Some(value.expression.variable!.value); - } - - return new Some(value.expression.literal.value); -} - -export function extractNumericLiteral (node?: SyntaxNode): number | null { - if (node instanceof PrimaryExpressionNode && node.expression instanceof LiteralNode) { - if (node.expression.literal?.kind === SyntaxTokenKind.NUMERIC_LITERAL) { - return Number(node.expression.literal.value); - } - } - return null; -} - -// Extract referee from a simple variable (x) or complex variable (a.b.c) -// For complex variables, returns the referee of the rightmost part -export function extractReferee (node?: SyntaxNode): NodeSymbol | undefined { - if (!node) return undefined; - - // Simple variable: x - if (isExpressionAVariableNode(node)) { - return node.referee; - } - - // Complex variable: a.b.c - get referee from rightmost part - if (node instanceof InfixExpressionNode && node.op?.value === '.') { - return extractReferee(node.rightExpression); - } - - return node.referee; -} - -export function isBinaryRelationship (value?: SyntaxNode): value is InfixExpressionNode { - if (!(value instanceof InfixExpressionNode)) { - return false; - } - - if (!isRelationshipOp(value.op?.value)) { - return false; - } - - return ( - destructureComplexVariableTuple(value.leftExpression) - .and_then(() => destructureComplexVariableTuple(value.rightExpression)) - .unwrap_or(undefined) !== undefined - ); -} - -export function isEqualTupleOperands (value: InfixExpressionNode): value is InfixExpressionNode { - const leftRes = destructureComplexVariableTuple(value.leftExpression); - const rightRes = destructureComplexVariableTuple(value.rightExpression); - - if (!leftRes.isOk() || !rightRes.isOk()) { - return false; - } - - const { tupleElements: leftTuple } = leftRes.unwrap(); - const { tupleElements: rightTuple } = rightRes.unwrap(); - - if (leftTuple?.length !== rightTuple?.length) { - return false; - } - - return true; -} - -export function isValidIndexName ( - value?: SyntaxNode, -): value is (PrimaryExpressionNode & { expression: VariableNode }) | FunctionExpressionNode { - return ( - (value instanceof PrimaryExpressionNode && value.expression instanceof VariableNode) - || value instanceof FunctionExpressionNode - ); -} - -export function extractIndexName ( - value: - | (PrimaryExpressionNode & { expression: VariableNode & { variable: SyntaxToken } }) - | (FunctionExpressionNode & { value: SyntaxToken }), -): string { - if (value instanceof PrimaryExpressionNode) { - return value.expression.variable.value; - } - - return value.value.value; -} - -// Destructure a call expression like `schema.table(col1, col2)` or `table(col1, col2)`. -// Returns the callee variables (schema, table) and the args (col1, col2). -// schema.table(col1, col2) => { variables: [schema, table], args: [col1, col2] } -// table(col1, col2) => { variables: [table], args: [col1, col2] } -// table() => { variables: [table], args: [] } -export function destructureCallExpression ( - node?: SyntaxNode, -): Option<{ variables: (PrimaryExpressionNode & { expression: VariableNode })[]; args: (PrimaryExpressionNode & { expression: VariableNode })[] }> { - if (!(node instanceof CallExpressionNode) || !node.callee) { - return new None(); - } - - // Destructure the callee (e.g., schema.table or just table) - const fragments = destructureMemberAccessExpression(node.callee).unwrap_or(undefined); - if (!fragments || fragments.length === 0) { - return new None(); - } - - // All callee fragments must be simple variables - if (!fragments.every(isExpressionAVariableNode)) { - return new None(); - } - - // Get args from argument list - let args: (PrimaryExpressionNode & { expression: VariableNode })[] = []; - if (isTupleOfVariables(node.argumentList)) { - args = [...node.argumentList.elementList]; - } - - return new Some({ - variables: fragments as (PrimaryExpressionNode & { expression: VariableNode })[], - args, - }); -} - -// Starting from `startElement` -// find the closest outer scope that contains `id` -// and return the symbol corresponding to `id` in that scope -export function findSymbol ( - id: NodeSymbolIndex, - startElement: ElementDeclarationNode, -): NodeSymbol | undefined { - let curElement: ElementDeclarationNode | ProgramNode | undefined = startElement; - const isPublicSchema = isPublicSchemaIndex(id); - - while (curElement) { - if (curElement.symbol?.symbolTable?.has(id)) { - return curElement.symbol.symbolTable?.get(id); - } - - if (curElement.symbol?.declaration instanceof ProgramNode && isPublicSchema) { - return curElement.symbol; - } - - if (curElement instanceof ProgramNode) { - return undefined; - } - - curElement = curElement.parent; - } - - return undefined; -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/checks.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/checks.ts deleted file mode 100644 index d865c345a..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/checks.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { last, partition } from 'lodash-es'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, - ElementDeclarationNode, - FunctionApplicationNode, - FunctionExpressionNode, - ListExpressionNode, - ProgramNode, - SyntaxNode, -} from '@/core/parser/nodes'; -import { isExpressionAQuotedString } from '@/core/parser/utils'; -import { aggregateSettingList, pickValidator } from '@/core/analyzer/validator/utils'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { getElementKind } from '@/core/analyzer/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { ElementKind } from '@/core/analyzer/types'; - -export default class ChecksValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - const invalidContextError = new CompileError( - CompileErrorCode.INVALID_CHECKS_CONTEXT, - 'A Checks can only appear inside a Table or a TablePartial', - this.declarationNode, - ); - if (this.declarationNode.parent instanceof ProgramNode) return [invalidContextError]; - - const elementKind = getElementKind(this.declarationNode.parent).unwrap_or(undefined); - return (elementKind && [ElementKind.Table, ElementKind.TablePartial].includes(elementKind)) - ? [] - : [invalidContextError]; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (nameNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'A Checks shouldn\'t have a name', nameNode)]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'A Checks shouldn\'t have an alias', aliasNode)]; - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - if (settingList) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Checks shouldn\'t have a setting list', settingList)]; - } - - return []; - } - - private validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A Checks must have a complex body', body)]; - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [...this.validateFields(fields as FunctionApplicationNode[]), ...this.validateSubElements(subs as ElementDeclarationNode[])]; - } - - private validateFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => { - if (!field.callee) { - return []; - } - - const errors: CompileError[] = []; - const args = [field.callee, ...field.args]; - if (last(args) instanceof ListExpressionNode) { - errors.push(...this.validateFieldSetting(args.pop() as ListExpressionNode)); - } - - if (args.length > 1 || !(args[0] instanceof FunctionExpressionNode)) { - errors.push(new CompileError(CompileErrorCode.INVALID_CHECKS_FIELD, 'A check field must be a function expression', field)); - } - - return errors; - }); - } - - private validateFieldSetting (settings: ListExpressionNode): CompileError[] { - const aggReport = aggregateSettingList(settings); - const errors = aggReport.getErrors(); - const settingMap = aggReport.getValue(); - - for (const name in settingMap) { - const attrs = settingMap[name]; - switch (name) { - case 'name': - if (attrs.length > 1) { - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_CHECK_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isExpressionAQuotedString(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_CHECK_SETTING_VALUE, `'${name}' must be a string`, attr)); - } - }); - break; - default: - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_CHECK_SETTING, `Unknown check setting '${name}'`, attr))); - } - } - return errors; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/custom.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/custom.ts deleted file mode 100644 index 1471655d7..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/custom.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, ProgramNode, SyntaxNode, -} from '@/core/parser/nodes'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { isExpressionAQuotedString } from '@/core/parser/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { getElementKind } from '@/core/analyzer/utils'; -import { ElementKind } from '@/core/analyzer/types'; - -export default class CustomValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - if (this.declarationNode.parent instanceof ProgramNode || getElementKind(this.declarationNode.parent).unwrap_or(undefined) !== ElementKind.Project) { - return [new CompileError(CompileErrorCode.INVALID_CUSTOM_CONTEXT, 'A Custom element can only appear in a Project', this.declarationNode)]; - } - return []; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (nameNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'A Custom element shouldn\'t have a name', nameNode)]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'A Custom element shouldn\'t have an alias', aliasNode)]; - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - if (settingList) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Custom element shouldn\'t have a setting list', settingList)]; - } - - return []; - } - - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - - if (body instanceof BlockExpressionNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_COMPLEX_BODY, 'A Custom element can only have an inline field', body)]; - } - - const errors: CompileError[] = []; - - if (!isExpressionAQuotedString(body.callee)) { - errors.push(new CompileError(CompileErrorCode.INVALID_CUSTOM_ELEMENT_VALUE, 'A Custom element value can only be a string', body)); - } - if (body.args.length > 0) { - errors.push(...body.args.map((arg) => new CompileError(CompileErrorCode.INVALID_CUSTOM_ELEMENT_VALUE, 'A Custom element value can only be a string', arg))); - } - - return errors; - } -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/enum.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/enum.ts deleted file mode 100644 index a4f9f74fe..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/enum.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { DEFAULT_SCHEMA_NAME } from '@/constants'; -import { last, partition } from 'lodash-es'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { isExpressionAQuotedString, isExpressionAVariableNode } from '@/core/parser/utils'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { - aggregateSettingList } from '@/core/analyzer/validator/utils'; -import { isValidName, pickValidator } from '@/core/analyzer/validator/utils'; -import { registerSchemaStack } from '@/core/analyzer/validator/utils'; -import { createEnumFieldSymbolIndex, createEnumSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; -import { destructureComplexVariable, extractVarNameFromPrimaryVariable } from '@/core/analyzer/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { EnumFieldSymbol, EnumSymbol } from '@/core/analyzer/symbol/symbols'; - -export default class EnumValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.registerElement(), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - if (this.declarationNode.parent instanceof ElementDeclarationNode) { - return [new CompileError(CompileErrorCode.INVALID_PROJECT_CONTEXT, 'An Enum can only appear top-level', this.declarationNode)]; - } - - return []; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (!nameNode) { - return [new CompileError(CompileErrorCode.NAME_NOT_FOUND, 'An Enum must have a name', this.declarationNode)]; - } - if (!isValidName(nameNode)) { - return [new CompileError(CompileErrorCode.INVALID_NAME, 'An Enum name must be of the form or .', nameNode)]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'An Enum shouldn\'t have an alias', aliasNode)]; - } - - return []; - } - - registerElement (): CompileError[] { - const errors: CompileError[] = []; - this.declarationNode.symbol = this.symbolFactory.create(EnumSymbol, { declaration: this.declarationNode, symbolTable: new SymbolTable() }); - const { name } = this.declarationNode; - - const maybeNameFragments = destructureComplexVariable(name); - if (maybeNameFragments.isOk()) { - const nameFragments = maybeNameFragments.unwrap(); - const enumName = nameFragments.pop()!; - const symbolTable = registerSchemaStack(nameFragments, this.publicSymbolTable, this.symbolFactory); - const enumId = createEnumSymbolIndex(enumName); - if (symbolTable.has(enumId)) { - errors.push(new CompileError(CompileErrorCode.DUPLICATE_NAME, `Enum name ${enumName} already exists in schema '${nameFragments.join('.') || DEFAULT_SCHEMA_NAME}'`, name!)); - } - symbolTable.set(enumId, this.declarationNode.symbol!); - } - - return errors; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - if (settingList) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'An Enum shouldn\'t have a setting list', settingList)]; - } - - return []; - } - - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.validateFields([body]); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [...this.validateFields(fields as FunctionApplicationNode[]), ...this.validateSubElements(subs as ElementDeclarationNode[])]; - } - - validateFields (fields: FunctionApplicationNode[]): CompileError[] { - if (fields.length === 0) { - return [new CompileError(CompileErrorCode.EMPTY_ENUM, 'An Enum must have at least one element', this.declarationNode)]; - } - - return fields.flatMap((field) => { - const errors: CompileError[] = []; - - if (field.callee && !isExpressionAVariableNode(field.callee)) { - errors.push(new CompileError(CompileErrorCode.INVALID_ENUM_ELEMENT_NAME, 'An enum field must be an identifier or a quoted identifier', field.callee)); - } - - const args = [...field.args]; - if (last(args) instanceof ListExpressionNode) { - errors.push(...this.validateFieldSettings(last(args) as ListExpressionNode)); - args.pop(); - } else if (args[0] instanceof ListExpressionNode) { - errors.push(...this.validateFieldSettings(args[0])); - args.shift(); - } - - if (args.length > 0) { - errors.push(...args.map((arg) => new CompileError(CompileErrorCode.INVALID_ENUM_ELEMENT, 'An Enum must have only a field and optionally a setting list', arg))); - } - - errors.push(...this.registerField(field)); - - return errors; - }); - } - - validateFieldSettings (settings: ListExpressionNode): CompileError[] { - const aggReport = aggregateSettingList(settings); - const errors = aggReport.getErrors(); - const settingMap = aggReport.getValue(); - - for (const name in settingMap) { - const attrs = settingMap[name]; - switch (name) { - case 'note': - if (attrs.length > 1) { - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_ENUM_ELEMENT_SETTING, '\'note\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isExpressionAQuotedString(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_ENUM_ELEMENT_SETTING, '\'note\' must be a string', attr)); - } - }); - break; - default: - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_ENUM_ELEMENT_SETTING, `Unknown enum field setting '${name}'`, attr))); - } - } - return errors; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - } - - registerField (field: FunctionApplicationNode): CompileError[] { - if (field.callee && isExpressionAVariableNode(field.callee)) { - const enumFieldName = extractVarNameFromPrimaryVariable(field.callee).unwrap(); - const enumFieldId = createEnumFieldSymbolIndex(enumFieldName); - - const enumSymbol = this.symbolFactory.create(EnumFieldSymbol, { declaration: field }); - field.symbol = enumSymbol; - - const symbolTable = this.declarationNode.symbol!.symbolTable!; - if (symbolTable.has(enumFieldId)) { - const symbol = symbolTable.get(enumFieldId); - return [ - new CompileError(CompileErrorCode.DUPLICATE_COLUMN_NAME, `Duplicate enum field ${enumFieldName}`, field), - new CompileError(CompileErrorCode.DUPLICATE_COLUMN_NAME, `Duplicate enum field ${enumFieldName}`, symbol!.declaration!), - ]; - } - symbolTable.set(enumFieldId, enumSymbol); - } - return []; - } -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/indexes.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/indexes.ts deleted file mode 100644 index e5e313c60..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/indexes.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { last, partition } from 'lodash-es'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, - CallExpressionNode, - ElementDeclarationNode, - FunctionApplicationNode, - ListExpressionNode, - PrimaryExpressionNode, - ProgramNode, - SyntaxNode, - VariableNode, -} from '@/core/parser/nodes'; -import { isExpressionAQuotedString, isExpressionAVariableNode } from '@/core/parser/utils'; -import { aggregateSettingList } from '@/core/analyzer/validator/utils'; -import { isVoid, pickValidator } from '@/core/analyzer/validator/utils'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { destructureIndexNode, getElementKind } from '@/core/analyzer/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { ElementKind } from '@/core/analyzer/types'; - -export default class IndexesValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - const invalidContextError = new CompileError( - CompileErrorCode.INVALID_INDEXES_CONTEXT, - 'An Indexes can only appear inside a Table or a TablePartial', - this.declarationNode, - ); - if (this.declarationNode.parent instanceof ProgramNode) return [invalidContextError]; - - const elementKind = getElementKind(this.declarationNode.parent).unwrap_or(undefined); - return (elementKind && [ElementKind.Table, ElementKind.TablePartial].includes(elementKind)) - ? [] - : [invalidContextError]; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (nameNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'An Indexes shouldn\'t have a name', nameNode)]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'An Indexes shouldn\'t have an alias', aliasNode)]; - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - if (settingList) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'An Indexes shouldn\'t have a setting list', settingList)]; - } - - return []; - } - - private validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'An Indexes must have a complex body', body)]; - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [...this.validateFields(fields as FunctionApplicationNode[]), ...this.validateSubElements(subs as ElementDeclarationNode[])]; - } - - private validateFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => { - if (!field.callee) { - return []; - } - - const errors: CompileError[] = []; - const args = [field.callee, ...field.args]; - if (last(args) instanceof ListExpressionNode) { - errors.push(...this.validateFieldSetting(args.pop() as ListExpressionNode)); - } - - args.forEach((sub) => { - // This is to deal with inline indexes field such as - // (id, name) (age, weight) - // which is parsed as a call expression - while (sub instanceof CallExpressionNode) { - if (sub.argumentList && !destructureIndexNode(sub.argumentList).isOk()) { - errors.push(new CompileError(CompileErrorCode.INVALID_INDEXES_FIELD, 'An index field must be an identifier, a quoted identifier, a functional expression or a tuple of such', sub.argumentList)); - } - sub = sub.callee!; - } - - if (!destructureIndexNode(sub).isOk()) { - errors.push(new CompileError(CompileErrorCode.INVALID_INDEXES_FIELD, 'An index field must be an identifier, a quoted identifier, a functional expression or a tuple of such', sub)); - } - }); - - return errors; - }); - } - - private validateFieldSetting (settings: ListExpressionNode): CompileError[] { - const aggReport = aggregateSettingList(settings); - const errors = aggReport.getErrors(); - const settingMap = aggReport.getValue(); - - for (const name in settingMap) { - const attrs = settingMap[name]; - switch (name) { - case 'note': - case 'name': - if (attrs.length > 1) { - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_INDEX_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isExpressionAQuotedString(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_INDEX_SETTING_VALUE, `'${name}' must be a string`, attr)); - } - }); - break; - case 'unique': - case 'pk': - if (attrs.length > 1) { - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_INDEX_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_INDEX_SETTING_VALUE, `'${name}' must not have a value`, attr)); - } - }); - break; - case 'type': - if (attrs.length > 1) { - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_INDEX_SETTING, '\'type\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isExpressionAVariableNode(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_INDEX_SETTING_VALUE, '\'type\' must be "btree" or "hash"', attr)); - } - }); - break; - default: - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_INDEX_SETTING, `Unknown index setting '${name}'`, attr))); - } - } - return errors; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - } -} - -export function isValidIndexesType (value?: SyntaxNode): boolean { - if (!(value instanceof PrimaryExpressionNode) || !(value.expression instanceof VariableNode)) { - return false; - } - - const str = value.expression.variable?.value; - - return str === 'btree' || str === 'hash'; -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/note.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/note.ts deleted file mode 100644 index a483e59d1..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/note.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { partition } from 'lodash-es'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, ProgramNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { isExpressionAQuotedString } from '@/core/parser/utils'; -import { pickValidator } from '@/core/analyzer/validator/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { ElementKind } from '@/core/analyzer/types'; -import { destructureComplexVariable, getElementKind } from '@/core/analyzer/utils'; -import { createStickyNoteSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; - -export default class NoteValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - if ( - !(this.declarationNode.parent instanceof ProgramNode) - && !( - [ - ElementKind.Table, - ElementKind.TableGroup, - ElementKind.TablePartial, - ElementKind.Project, - ] as (ElementKind | undefined)[] - ) - .includes(getElementKind(this.declarationNode.parent).unwrap_or(undefined)) - ) { - return [new CompileError( - CompileErrorCode.INVALID_NOTE_CONTEXT, - 'A Note can only appear inside a Table, a TableGroup, a TablePartial or a Project. Sticky note can only appear at the global scope.', - this.declarationNode, - )]; - } - - return []; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (!(this.declarationNode.parent instanceof ProgramNode)) { - if (nameNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'A Note shouldn\'t have a name', nameNode)]; - } - return []; - } - - if (!nameNode) { - return [new CompileError(CompileErrorCode.INVALID_NAME, 'Sticky note must have a name', this.declarationNode)]; - } - - const nameFragments = destructureComplexVariable(nameNode); - if (!nameFragments.isOk()) return [new CompileError(CompileErrorCode.INVALID_NAME, 'Invalid name for sticky note ', this.declarationNode)]; - - const names = nameFragments.unwrap(); - - const trueName = names.join('.'); - - const noteId = createStickyNoteSymbolIndex(trueName); - - if (this.publicSymbolTable.has(noteId)) { - return [new CompileError(CompileErrorCode.DUPLICATE_NAME, `Sticky note "${trueName}" has already been defined`, nameNode)]; - } - - this.publicSymbolTable.set(noteId, this.declarationNode.symbol!); - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'A Ref shouldn\'t have an alias', aliasNode)]; - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - if (settingList) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Note shouldn\'t have a setting list', settingList)]; - } - - return []; - } - - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.validateFields([body]); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [...this.validateFields(fields as FunctionApplicationNode[]), ...this.validateSubElements(subs as ElementDeclarationNode[])]; - } - - validateFields (fields: FunctionApplicationNode[]): CompileError[] { - const errors: CompileError[] = []; - if (fields.length === 0) { - return [new CompileError(CompileErrorCode.EMPTY_NOTE, 'A Note must have a content', this.declarationNode)]; - } - if (fields.length > 1) { - fields.slice(1).forEach((field) => errors.push(new CompileError(CompileErrorCode.NOTE_CONTENT_REDEFINED, 'A Note can only contain one string', field))); - } - if (!isExpressionAQuotedString(fields[0].callee)) { - errors.push(new CompileError(CompileErrorCode.INVALID_NOTE, 'A Note content must be a quoted string', fields[0])); - } - if (fields[0].args.length > 0) { - errors.push(...fields[0].args.map((arg) => new CompileError(CompileErrorCode.INVALID_NOTE, 'A Note can only contain one quoted string', arg))); - } - return errors; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/project.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/project.ts deleted file mode 100644 index a6fae1f79..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/project.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { partition } from 'lodash-es'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { isSimpleName, pickValidator } from '@/core/analyzer/validator/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; - -export default class ProjectValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [...this.validateContext(), ...this.validateName(this.declarationNode.name), ...this.validateAlias(this.declarationNode.alias), ...this.validateSettingList(this.declarationNode.attributeList), ...this.validateBody(this.declarationNode.body)]; - } - - private validateContext (): CompileError[] { - if (this.declarationNode.parent instanceof ElementDeclarationNode) { - return [new CompileError(CompileErrorCode.INVALID_PROJECT_CONTEXT, 'A Project can only appear top-level', this.declarationNode)]; - } - - return []; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (!nameNode) { - return []; - } - - if (!isSimpleName(nameNode)) { - return [new CompileError(CompileErrorCode.INVALID_NAME, 'A Project\'s name is optional or must be an identifier or a quoted identifer', nameNode)]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'A Project shouldn\'t have an alias', aliasNode)]; - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - if (settingList) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Project shouldn\'t have a setting list', settingList)]; - } - - return []; - } - - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A Project\'s body must be a block', body)]; - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [ - ...fields.map((field) => new CompileError(CompileErrorCode.INVALID_PROJECT_FIELD, 'A Project can not have inline fields', field)), - ...this.validateSubElements(subs as ElementDeclarationNode[]), - ]; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/records.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/records.ts deleted file mode 100644 index 22da5b517..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/records.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { partition } from 'lodash-es'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, CallExpressionNode, CommaExpressionNode, ElementDeclarationNode, EmptyNode, FunctionApplicationNode, FunctionExpressionNode, ListExpressionNode, ProgramNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { isExpressionASignedNumberExpression, isTupleOfVariables, isValidName, pickValidator } from '@/core/analyzer/validator/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { destructureComplexVariable, getElementKind } from '@/core/analyzer/utils'; -import { ElementKind } from '@/core/analyzer/types'; -import { isAccessExpression, isExpressionAQuotedString, isExpressionAVariableNode } from '@/core/parser/utils'; -import { KEYWORDS_OF_DEFAULT_SETTING } from '@/constants'; - -export default class RecordsValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [...this.validateContext(), ...this.validateName(this.declarationNode.name), ...this.validateAlias(this.declarationNode.alias), ...this.validateSettingList(this.declarationNode.attributeList), ...this.validateBody(this.declarationNode.body)]; - } - - // Validate that Records can only appear top-level or inside a Table. - // Valid: - // records users(id, name) { ... } // top-level - // table users { records (id, name) { } } // inside a table - // Invalid: - // enum status { records { } } // inside an enum - // indexes { records { } } // inside indexes - private validateContext (): CompileError[] { - const parent = this.declarationNode.parent; - const isTopLevel = parent instanceof ProgramNode; - - if (isTopLevel) { - return []; - } - - // Check if parent is a table - if (parent instanceof ElementDeclarationNode) { - const elementKind = getElementKind(parent).unwrap_or(undefined); - if (elementKind === ElementKind.Table) { - return []; - } - } - - return [new CompileError( - CompileErrorCode.INVALID_RECORDS_CONTEXT, - 'Records can only appear at top-level or inside a Table', - this.declarationNode, - )]; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - const parent = this.declarationNode.parent; - const isTopLevel = parent instanceof ProgramNode; - - return isTopLevel - ? this.validateTopLevelName(nameNode) - : this.validateInsideTableName(nameNode); - } - - // At top-level - must reference a table with column list: - // Valid: records users(id, name, email) { } - // Valid: records myschema.users(id, name) { } - // Invalid: records users { } // missing column list - // Invalid: records { } // missing table reference - private validateTopLevelName (nameNode?: SyntaxNode): CompileError[] { - if (!(nameNode instanceof CallExpressionNode)) { - return [new CompileError( - CompileErrorCode.INVALID_RECORDS_NAME, - 'Records at top-level must have a name in the form of table(col1, col2, ...) or schema.table(col1, col2, ...)', - nameNode || this.declarationNode.type, - )]; - } - - const errors: CompileError[] = []; - - // Validate callee is a valid name (simple or complex variable like schema.table) - if (!nameNode.callee || !isValidName(nameNode.callee)) { - errors.push(new CompileError( - CompileErrorCode.INVALID_RECORDS_NAME, - 'Records table reference must be a valid table name', - nameNode.callee || nameNode, - )); - } - - // Validate argument list is a tuple of simple variables - if (!nameNode.argumentList || !isTupleOfVariables(nameNode.argumentList)) { - errors.push(new CompileError( - CompileErrorCode.INVALID_RECORDS_NAME, - 'Records column list must be simple column names', - nameNode.argumentList || nameNode, - )); - } - - return errors; - } - - // Inside a table - optional column list only: - // Valid: records (id, name) { } - // Valid: records { } // all columns - // Invalid: records other_table(id) { } // can't reference another table - private validateInsideTableName (nameNode?: SyntaxNode): CompileError[] { - if (nameNode && !isTupleOfVariables(nameNode)) { - return [new CompileError( - CompileErrorCode.INVALID_RECORDS_NAME, - 'Records inside a Table can only have a column list like (col1, col2, ...)', - nameNode, - )]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'Records cannot have an alias', aliasNode)]; - } - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - if (settingList) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'Records cannot have a setting list', settingList)]; - } - return []; - } - - // Validate that records body contains only simple values (one comma-separated row per line). - // Valid values: - // 1, 2, 3 // numbers - // -5, +10 // signed numbers - // 'hello', "world" // quoted strings - // `backtick string` // function expression (backtick string) - // true, false, TRUE, FALSE // booleans - // null, NULL // null - // ,, , // empty values (consecutive commas) - // status.active // enum field reference - // myschema.status.pending // schema.enum.field reference - // Invalid values: - // 2 + 1, 3 * 2 // arithmetic expressions - // func() // function calls - // (1, 2) // nested tuples - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.validateDataRow(body); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [ - ...this.validateDataRows(fields as FunctionApplicationNode[]), - ...this.validateSubElements(subs as ElementDeclarationNode[]), - ]; - } - - private validateDataRows (rows: FunctionApplicationNode[]): CompileError[] { - return rows.flatMap((row) => this.validateDataRow(row)); - } - - // Validate a single data row. Structure should be: - // row.callee = CommaExpressionNode (e.g., 1, 'hello', true) or single value (e.g., 1) - // row.args = [] (empty) - private validateDataRow (row: FunctionApplicationNode): CompileError[] { - const errors: CompileError[] = []; - - // Callee must exist & Args should be empty - all values should be in callee as a comma expression - if (!row.callee || row.args.length > 0) { - errors.push(new CompileError( - CompileErrorCode.INVALID_RECORDS_FIELD, - 'Invalid record row structure', - row, - )); - return errors; - } - - // Callee should be either a CommaExpressionNode or a single valid value - if (row.callee instanceof CommaExpressionNode) { - // Validate each element in the comma expression - for (const value of row.callee.elementList) { - if (!this.isValidRecordValue(value)) { - errors.push(new CompileError( - CompileErrorCode.INVALID_RECORDS_FIELD, - 'Records can only contain simple values (literals, null, true, false, or enum references). Complex expressions are not allowed.', - value, - )); - } - } - } else { - // Single value (no comma) - if (!this.isValidRecordValue(row.callee)) { - errors.push(new CompileError( - CompileErrorCode.INVALID_RECORDS_FIELD, - 'Records can only contain simple values (literals, null, true, false, or enum references). Complex expressions are not allowed.', - row.callee, - )); - } - } - - return errors; - } - - // Check if a value is valid for a record field. - private isValidRecordValue (value: SyntaxNode): boolean { - // Empty values from consecutive commas: 1,,3 or ,1,2 - if (value instanceof EmptyNode) { - return true; - } - - // Signed numbers: -2, +5, 42, 3.14 - if (isExpressionASignedNumberExpression(value)) { - return true; - } - - // Quoted strings: 'single', "double" - if (isExpressionAQuotedString(value)) { - return true; - } - - // Backtick strings: `hello world` - if (value instanceof FunctionExpressionNode) { - return true; - } - - // Simple identifiers: only null, true, false (case-insensitive) - if (isExpressionAVariableNode(value)) { - const identifierValue = value.expression.variable.value.toLowerCase(); - return KEYWORDS_OF_DEFAULT_SETTING.includes(identifierValue); - } - - // Member access for enum field references: status.active, myschema.status.pending - if (isAccessExpression(value)) { - const fragments = destructureComplexVariable(value).unwrap_or(undefined); - return fragments !== undefined && fragments.length > 0; - } - - return false; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - } -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/ref.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/ref.ts deleted file mode 100644 index cb2a999ad..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/ref.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { partition, last } from 'lodash-es'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, IdentiferStreamNode, ListExpressionNode, ProgramNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { - extractStringFromIdentifierStream, - isExpressionAVariableNode, -} from '@/core/parser/utils'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { isSimpleName, isValidColor, pickValidator, aggregateSettingList } from '@/core/analyzer/validator/utils'; -import { destructureComplexVariable, destructureComplexVariableTuple, isBinaryRelationship, isEqualTupleOperands } from '@/core/analyzer/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; - -export default class RefValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - if (this.declarationNode.parent instanceof ProgramNode) { - return []; - } - return [new CompileError(CompileErrorCode.INVALID_REF_CONTEXT, 'A Ref must appear top-level', this.declarationNode)]; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (!nameNode) { - return []; - } - - if (!isSimpleName(nameNode)) { - return [new CompileError(CompileErrorCode.INVALID_NAME, 'A Ref\'s name is optional or must be an identifier or a quoted identifer', nameNode)]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'A Ref shouldn\'t have an alias', aliasNode)]; - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - if (settingList) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Ref shouldn\'t have a setting list', settingList)]; - } - - return []; - } - - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return this.validateFields([body]); - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [...this.validateFields(fields as FunctionApplicationNode[]), ...this.validateSubElements(subs as ElementDeclarationNode[])]; - } - - validateFields (fields: FunctionApplicationNode[]): CompileError[] { - if (fields.length === 0) { - return [new CompileError(CompileErrorCode.EMPTY_REF, 'A Ref must have at least one field', this.declarationNode)]; - } - - const errors: CompileError[] = []; - if (fields.length > 1) { - errors.push(...fields.slice(1).map((field) => new CompileError(CompileErrorCode.REF_REDEFINED, 'A Ref can only contain one binary relationship', field))); - } - - fields.forEach((field) => { - if (field.callee && !isBinaryRelationship(field.callee)) { - errors.push(new CompileError(CompileErrorCode.INVALID_REF_FIELD, 'A Ref field must be a binary relationship', field.callee)); - } - - if (field.callee && isBinaryRelationship(field.callee)) { - const leftFragment = destructureComplexVariableTuple(field.callee.leftExpression).unwrap_or({ variables: [], tupleElements: [] }); - const leftFragmentCount = leftFragment.variables.length + Math.min(leftFragment.tupleElements.length, 1); - const rightFragment = destructureComplexVariableTuple(field.callee.rightExpression).unwrap_or({ variables: [], tupleElements: [] }); - const rightFragmentCount = rightFragment.variables.length + Math.min(rightFragment.tupleElements.length, 1); - if (leftFragmentCount < 2) { - errors.push(new CompileError(CompileErrorCode.INVALID_REF_FIELD, 'Invalid column reference', field.callee.leftExpression || field.callee)); - } - if (rightFragmentCount < 2) { - errors.push(new CompileError(CompileErrorCode.INVALID_REF_FIELD, 'Invalid column reference', field.callee.rightExpression || field.callee)); - } - } - - if (field.callee && !isEqualTupleOperands(field.callee)) { - errors.push(new CompileError(CompileErrorCode.UNEQUAL_FIELDS_BINARY_REF, 'Unequal fields in ref endpoints', field.callee)); - } - - const args = [...field.args]; - if (last(args) instanceof ListExpressionNode) { - const errs = this.validateFieldSettings(last(args) as ListExpressionNode); - errors.push(...errs); - args.pop(); - } else if (args[0] instanceof ListExpressionNode) { - errors.push(...this.validateFieldSettings(args[0])); - args.shift(); - } - - if (args.length > 0) { - errors.push(...args.map((arg) => new CompileError(CompileErrorCode.INVALID_REF_FIELD, 'A Ref field should only have a single binary relationship', arg))); - } - }); - - return errors; - } - - validateFieldSettings (settings: ListExpressionNode): CompileError[] { - const aggReport = aggregateSettingList(settings); - const errors = aggReport.getErrors(); - const settingMap = aggReport.getValue(); - for (const name in settingMap) { - const attrs = settingMap[name]; - switch (name) { - case 'delete': - case 'update': - if (attrs.length > 1) { - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_REF_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isValidPolicy(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_REF_SETTING_VALUE, `'${name}' can only have values "cascade", "no action", "set null", "set default" or "restrict"`, attr)); - } - }); - break; - case 'color': - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_REF_SETTING, '\'color\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isValidColor(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_REF_SETTING_VALUE, '\'color\' must be a color literal', attr!)); - } - }); - break; - default: - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_REF_SETTING, `Unknown ref setting '${name}'`, attr))); - } - } - return errors; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - } -} - -function isValidPolicy (value?: SyntaxNode): boolean { - if ( - !( - isExpressionAVariableNode(value) - && value.expression.variable.kind !== SyntaxTokenKind.QUOTED_STRING - ) - && !(value instanceof IdentiferStreamNode) - ) { - return false; - } - - let extractedString: string | undefined; - if (value instanceof IdentiferStreamNode) { - extractedString = extractStringFromIdentifierStream(value).unwrap_or(''); - } else { - extractedString = value.expression.variable.value; - } - - if (extractedString) { - switch (extractedString.toLowerCase()) { - case 'cascade': - case 'no action': - case 'set null': - case 'set default': - case 'restrict': - return true; - default: - return false; - } - } - - return false; // unreachable -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/table.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/table.ts deleted file mode 100644 index 56443d91f..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/table.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { DEFAULT_SCHEMA_NAME } from '@/constants'; -import { last, forIn, partition } from 'lodash-es'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - ArrayNode, - AttributeNode, - BlockExpressionNode, - ElementDeclarationNode, - ExpressionNode, - FunctionApplicationNode, - FunctionExpressionNode, - ListExpressionNode, - PrefixExpressionNode, - PrimaryExpressionNode, - SyntaxNode, -} from '@/core/parser/nodes'; -import { destructureComplexVariable, extractVariableFromExpression, extractVarNameFromPrimaryVariable } from '@/core/analyzer/utils'; -import { - aggregateSettingList, - isSimpleName, - isUnaryRelationship, - isValidAlias, - isValidColor, - isValidColumnType, - isValidDefaultValue, - isValidName, - isValidPartialInjection, - isVoid, - pickValidator, - registerSchemaStack, -} from '@/core/analyzer/validator/utils'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { ColumnSymbol, PartialInjectionSymbol, TableSymbol } from '@/core/analyzer/symbol/symbols'; -import { createColumnSymbolIndex, createPartialInjectionSymbolIndex, createTableSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; -import { - isExpressionAQuotedString, - isExpressionAVariableNode, - isExpressionAnIdentifierNode, -} from '@/core/parser/utils'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { SettingName } from '@/core/analyzer/types'; - -export default class TableValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private symbolFactory: SymbolFactory; - private publicSymbolTable: SymbolTable; - - constructor ( - declarationNode: ElementDeclarationNode & { type: SyntaxToken }, - publicSymbolTable: SymbolTable, - symbolFactory: SymbolFactory, - ) { - this.declarationNode = declarationNode; - this.symbolFactory = symbolFactory; - this.publicSymbolTable = publicSymbolTable; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.registerElement(), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - if (this.declarationNode.parent instanceof ElementDeclarationNode) { - return [new CompileError(CompileErrorCode.INVALID_TABLE_CONTEXT, 'Table must appear top-level', this.declarationNode)]; - } - return []; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (!nameNode) { - return [new CompileError(CompileErrorCode.NAME_NOT_FOUND, 'A Table must have a name', this.declarationNode)]; - } - if (nameNode instanceof ArrayNode) { - return [new CompileError(CompileErrorCode.INVALID_NAME, 'Invalid array as Table name, maybe you forget to add a space between the name and the setting list?', nameNode)]; - } - if (!isValidName(nameNode)) { - return [new CompileError(CompileErrorCode.INVALID_NAME, 'A Table name must be of the form or .
', nameNode)]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (!aliasNode) { - return []; - } - - if (!isValidAlias(aliasNode)) { - return [new CompileError(CompileErrorCode.INVALID_ALIAS, 'Table aliases can only contains alphanumeric and underscore unless surrounded by double quotes', aliasNode)]; - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - const aggReport = aggregateSettingList(settingList); - const errors = aggReport.getErrors(); - const settingMap = aggReport.getValue(); - - forIn(settingMap, (attrs, name) => { - switch (name) { - case SettingName.HeaderColor: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_TABLE_SETTING, '\'headercolor\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isValidColor(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_SETTING_VALUE, '\'headercolor\' must be a color literal', attr.value || attr.name!)); - } - }); - break; - case SettingName.Note: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_TABLE_SETTING, '\'note\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isExpressionAQuotedString(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_SETTING_VALUE, '\'note\' must be a string literal', attr.value || attr.name!)); - } - }); - break; - default: - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.UNKNOWN_TABLE_SETTING, `Unknown '${name}' setting`, attr))); - } - }); - return errors; - } - - registerElement (): CompileError[] { - const errors: CompileError[] = []; - this.declarationNode.symbol = this.symbolFactory.create(TableSymbol, { declaration: this.declarationNode, symbolTable: new SymbolTable() }); - - const { name, alias } = this.declarationNode; - - const maybeNameFragments = destructureComplexVariable(name); - if (maybeNameFragments.isOk()) { - const nameFragments = [...maybeNameFragments.unwrap()]; - const tableName = nameFragments.pop()!; - const symbolTable = registerSchemaStack(nameFragments, this.publicSymbolTable, this.symbolFactory); - const tableId = createTableSymbolIndex(tableName); - if (symbolTable.has(tableId)) { - errors.push(new CompileError(CompileErrorCode.DUPLICATE_NAME, `Table name '${tableName}' already exists in schema '${nameFragments.join('.') || DEFAULT_SCHEMA_NAME}'`, name!)); - } - symbolTable.set(tableId, this.declarationNode.symbol!); - } - - if ( - alias && isSimpleName(alias) - - && !isAliasSameAsName(alias.expression.variable!.value, maybeNameFragments.unwrap_or([])) - ) { - const aliasName = extractVarNameFromPrimaryVariable(alias as any).unwrap(); - const aliasId = createTableSymbolIndex(aliasName); - if (this.publicSymbolTable.has(aliasId)) { - errors.push(new CompileError(CompileErrorCode.DUPLICATE_NAME, `Table name '${aliasName}' already exists`, name!)); - } - this.publicSymbolTable.set(aliasId, this.declarationNode.symbol!); - } - - return errors; - } - - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) { - return []; - } - if (body instanceof FunctionApplicationNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A Table\'s body must be a block', body)]; - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [ - ...this.validateFields(fields as FunctionApplicationNode[]), - ...this.validateSubElements(subs as ElementDeclarationNode[]), - ]; - } - - validateFields (fields: FunctionApplicationNode[]): CompileError[] { - const validateColumn = (field: FunctionApplicationNode) => { - const errors: CompileError[] = []; - if (!field.callee) { - return []; - } - if (field.args.length === 0) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN, 'A column must have a type', field.callee!)); - } - - if (!isExpressionAVariableNode(field.callee)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_NAME, 'A column name must be an identifier or a quoted identifier', field.callee)); - } - - if (field.args[0] && !isValidColumnType(field.args[0])) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_TYPE, 'Invalid column type', field.args[0])); - } - - const remains = field.args.slice(1); - errors.push(...this.validateFieldSetting(remains)); - - errors.push(...this.registerField(field)); - - return errors; - }; - const validatePartialInjection = (field: FunctionApplicationNode) => { - const errors: CompileError[] = []; - if (!field.callee) { - return []; - } - if (!isValidPartialInjection(field.callee)) { - errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_PARTIAL_INJECTION, 'A partial injection should be of the form ~', field.callee)); - } else { - const injectedTablePartialName = extractVariableFromExpression(field.callee.expression).unwrap_or(''); - const partialInjectionSymbol = this.symbolFactory.create(PartialInjectionSymbol, { symbolTable: new SymbolTable(), declaration: field }); - const partialInjectionSymbolId = createPartialInjectionSymbolIndex(injectedTablePartialName); - const symbolTable = this.declarationNode.symbol!.symbolTable!; - if (symbolTable.has(partialInjectionSymbolId)) { - const symbol = symbolTable.get(partialInjectionSymbolId); - return [ - new CompileError(CompileErrorCode.DUPLICATE_TABLE_PARTIAL_INJECTION_NAME, `Duplicate table partial injection '${injectedTablePartialName}'`, field), - new CompileError(CompileErrorCode.DUPLICATE_TABLE_PARTIAL_INJECTION_NAME, `Duplicate table partial injection '${injectedTablePartialName}'`, symbol!.declaration!), - ]; - } - symbolTable.set(partialInjectionSymbolId, partialInjectionSymbol); - } - if (field.args.length) { - errors.push( - ...field.args.map((arg) => new CompileError(CompileErrorCode.INVALID_TABLE_PARTIAL_INJECTION, 'A partial injection does not have any trailing attributes', arg)), - ); - } - return errors; - }; - return fields.flatMap((field) => { - if (field.callee instanceof PrefixExpressionNode && field.callee.op?.value === '~') return validatePartialInjection(field); - return validateColumn(field); - }); - } - - registerField (field: FunctionApplicationNode): CompileError[] { - if (field.callee && isExpressionAVariableNode(field.callee)) { - const columnName = extractVarNameFromPrimaryVariable(field.callee).unwrap(); - const columnId = createColumnSymbolIndex(columnName); - - const columnSymbol = this.symbolFactory.create(ColumnSymbol, { declaration: field }); - field.symbol = columnSymbol; - - const symbolTable = this.declarationNode.symbol!.symbolTable!; - if (symbolTable.has(columnId)) { - const symbol = symbolTable.get(columnId); - return [ - new CompileError(CompileErrorCode.DUPLICATE_COLUMN_NAME, `Duplicate column ${columnName}`, field), - new CompileError(CompileErrorCode.DUPLICATE_COLUMN_NAME, `Duplicate column ${columnName}`, symbol!.declaration!), - ]; - } - symbolTable.set(columnId, columnSymbol); - } - return []; - } - - // This is needed to support legacy inline settings - validateFieldSetting (parts: ExpressionNode[]): CompileError[] { - if (!parts.slice(0, -1).every(isExpressionAnIdentifierNode) || !parts.slice(-1).every((p) => isExpressionAnIdentifierNode(p) || p instanceof ListExpressionNode)) { - return [...parts.map((part) => new CompileError(CompileErrorCode.INVALID_COLUMN, 'These fields must be some inline settings optionally ended with a setting list', part))]; - } - - if (parts.length === 0) { - return []; - } - - let settingList: ListExpressionNode | undefined; - if (last(parts) instanceof ListExpressionNode) { - settingList = parts.pop() as ListExpressionNode; - } - - const aggReport = aggregateSettingList(settingList); - const errors = aggReport.getErrors(); - const settingMap: { - [index: string]: AttributeNode[]; - } & { - pk?: (AttributeNode | PrimaryExpressionNode)[]; - unique?: (AttributeNode | PrimaryExpressionNode)[]; - } = aggReport.getValue(); - - parts.forEach((part) => { - const name = extractVarNameFromPrimaryVariable(part as any).unwrap_or('').toLowerCase(); - if (name !== 'pk' && name !== 'unique') { - errors.push(new CompileError(CompileErrorCode.INVALID_SETTINGS, 'Inline column settings can only be `pk` or `unique`', part)); - return; - } - if (settingMap[name] === undefined) { - settingMap[name] = [part as PrimaryExpressionNode]; - } else { - settingMap[name]!.push(part as PrimaryExpressionNode); - } - }); - - const pkAttrs = settingMap[SettingName.PK] || []; - const pkeyAttrs = settingMap[SettingName.PrimaryKey] || []; - if (pkAttrs.length >= 1 && pkeyAttrs.length >= 1) { - errors.push( - ...[...pkAttrs, ...pkeyAttrs] - .map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, 'Either one of \'primary key\' and \'pk\' can appear', attr)), - ); - } - - forIn(settingMap, (attrs, name) => { - switch (name) { - case SettingName.Note: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, 'note can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isExpressionAQuotedString(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'note\' must be a quoted string', attr.value || attr.name!)); - } - }); - break; - case SettingName.Ref: - attrs.forEach((attr) => { - if (!isUnaryRelationship(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'ref\' must be a valid unary relationship', attr.value || attr.name!)); - } - }); - break; - case SettingName.PrimaryKey: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, 'primary key can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'primary key\' must not have a value', attr.value || attr.name!)); - } - }); - break; - case SettingName.PK: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'pk\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (attr instanceof AttributeNode && !isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'pk\' must not have a value', attr.value || attr.name!)); - } - }); - break; - case SettingName.NotNull: { - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'not null\' can only appear once', attr))); - } - const nullAttrs = settingMap[SettingName.Null] || []; - if (attrs.length >= 1 && nullAttrs.length >= 1) { - errors.push( - ...[...attrs, ...nullAttrs] - .map((attr) => new CompileError(CompileErrorCode.CONFLICTING_SETTING, '\'not null\' and \'null\' can not be set at the same time', attr)), - ); - } - attrs.forEach((attr) => { - if (!isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'not null\' must not have a value', attr.value || attr.name!)); - } - }); - break; - } - case SettingName.Null: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'null\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'null\' must not have a value', attr.value || attr.name!)); - } - }); - break; - case SettingName.Unique: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'unique\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (attr instanceof AttributeNode && !isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'unique\' must not have a value', attr.value || attr.name!)); - } - }); - break; - case SettingName.Increment: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'increment\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (attr instanceof AttributeNode && !isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'increment\' must not have a value', attr.value || attr.name!)); - } - }); - break; - case SettingName.Default: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'default\' can only appear once', attr))); - } - attrs.forEach((attr) => { - if (!isValidDefaultValue(attr.value)) { - errors.push(new CompileError( - CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, - '\'default\' must be an enum value, a string literal, number literal, function expression, true, false or null', - attr.value || attr.name!, - )); - } - }); - break; - case SettingName.Check: - attrs.forEach((attr) => { - if (!(attr.value instanceof FunctionExpressionNode)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'check\' must be a function expression', attr.value || attr.name!)); - } - }); - break; - - default: - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_COLUMN_SETTING, `Unknown column setting '${name}'`, attr))); - } - }); - return errors; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - const errors = subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - - const notes = subs.filter((sub) => sub.type?.value.toLowerCase() === 'note'); - if (notes.length > 1) { - errors.push(...notes.map((note) => new CompileError(CompileErrorCode.NOTE_REDEFINED, 'Duplicate notes are defined', note))); - } - - return errors; - } -} - -function isAliasSameAsName (alias: string, nameFragments: string[]): boolean { - return nameFragments.length === 1 && alias === nameFragments[0]; -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/tableGroup.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/tableGroup.ts deleted file mode 100644 index c63c8e6d0..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/tableGroup.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { forIn, partition } from 'lodash-es'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - isSimpleName, pickValidator } from '@/core/analyzer/validator/utils'; -import { isValidColor, registerSchemaStack, aggregateSettingList } from '@/core/analyzer/validator/utils'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, SyntaxNode, -} from '@/core/parser/nodes'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { createTableGroupFieldSymbolIndex, createTableGroupSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; -import { destructureComplexVariable, extractVarNameFromPrimaryVariable } from '@/core/analyzer/utils'; -import { TableGroupFieldSymbol, TableGroupSymbol } from '@/core/analyzer/symbol/symbols'; -import { isExpressionAVariableNode, isExpressionAQuotedString } from '@/core/parser/utils'; - -export default class TableGroupValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - private publicSymbolTable: SymbolTable; - private symbolFactory: SymbolFactory; - - constructor (declarationNode: ElementDeclarationNode & { type: SyntaxToken }, publicSymbolTable: SymbolTable, symbolFactory: SymbolFactory) { - this.declarationNode = declarationNode; - this.publicSymbolTable = publicSymbolTable; - this.symbolFactory = symbolFactory; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.registerElement(), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - if (this.declarationNode.parent instanceof ElementDeclarationNode) { - return [new CompileError( - CompileErrorCode.INVALID_TABLEGROUP_CONTEXT, - 'TableGroup must appear top-level', - this.declarationNode, - )]; - } - return []; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (!nameNode) { - return [new CompileError( - CompileErrorCode.NAME_NOT_FOUND, - 'A TableGroup must have a name', - this.declarationNode, - )]; - } - if (!isSimpleName(nameNode)) { - return [new CompileError( - CompileErrorCode.INVALID_NAME, - 'A TableGroup name must be a single identifier', - nameNode, - )]; - } - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError( - CompileErrorCode.UNEXPECTED_ALIAS, - 'A TableGroup shouldn\'t have an alias', - aliasNode, - )]; - } - - return []; - } - - registerElement (): CompileError[] { - const { name } = this.declarationNode; - this.declarationNode.symbol = this.symbolFactory.create(TableGroupSymbol, { declaration: this.declarationNode, symbolTable: new SymbolTable() }); - const maybeNameFragments = destructureComplexVariable(name); - if (maybeNameFragments.isOk()) { - const nameFragments = maybeNameFragments.unwrap(); - const tableGroupName = nameFragments.pop()!; - const symbolTable = registerSchemaStack(nameFragments, this.publicSymbolTable, this.symbolFactory); - const tableId = createTableGroupSymbolIndex(tableGroupName); - if (symbolTable.has(tableId)) { - return [new CompileError(CompileErrorCode.DUPLICATE_NAME, `TableGroup name '${tableGroupName}' already exists`, name!)]; - } - symbolTable.set(tableId, this.declarationNode.symbol!); - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - const aggReport = aggregateSettingList(settingList); - const errors = aggReport.getErrors(); - const settingMap = aggReport.getValue(); - - forIn(settingMap, (attrs, name) => { - switch (name) { - case 'color': - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError( - CompileErrorCode.DUPLICATE_TABLE_SETTING, - '\'color\' can only appear once', - attr, - ))); - } - attrs.forEach((attr) => { - if (!isValidColor(attr.value)) { - errors.push(new CompileError( - CompileErrorCode.INVALID_TABLE_SETTING_VALUE, - '\'color\' must be a color literal', - attr.value || attr.name!, - )); - } - }); - break; - case 'note': - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError( - CompileErrorCode.DUPLICATE_TABLE_SETTING, - '\'note\' can only appear once', - attr, - ))); - } - attrs - .filter((attr) => !isExpressionAQuotedString(attr.value)) - .forEach((attr) => { - errors.push(new CompileError( - CompileErrorCode.INVALID_TABLE_SETTING_VALUE, - '\'note\' must be a string literal', - attr.value || attr.name!, - )); - }); - break; - default: - errors.push(...attrs.map((attr) => new CompileError( - CompileErrorCode.UNKNOWN_TABLE_SETTING, - `Unknown '${name}' setting`, - attr, - ))); - break; - } - }); - return errors; - } - - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) return []; - - if (body instanceof FunctionApplicationNode) { - return [new CompileError( - CompileErrorCode.UNEXPECTED_SIMPLE_BODY, - 'A TableGroup\'s body must be a block', - body, - )]; - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [ - ...this.validateFields(fields as FunctionApplicationNode[]), - ...this.validateSubElements(subs as ElementDeclarationNode[]), - ]; - } - - validateFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => { - const errors: CompileError[] = []; - if (field.callee && !destructureComplexVariable(field.callee).isOk()) { - errors.push(new CompileError(CompileErrorCode.INVALID_TABLEGROUP_FIELD, 'A TableGroup field must be of the form
or .
', field.callee)); - } - - this.registerField(field); - - if (field.args.length > 0) { - errors.push(...field.args.map((arg) => new CompileError(CompileErrorCode.INVALID_TABLEGROUP_FIELD, 'A TableGroup field should only have a single Table name', arg))); - } - - return errors; - }); - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - const errors = subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - - const notes = subs.filter((sub) => sub.type?.value.toLowerCase() === 'note'); - if (notes.length > 1) errors.push(...notes.map((note) => new CompileError(CompileErrorCode.NOTE_REDEFINED, 'Duplicate notes are defined', note))); - return errors; - } - - registerField (field: FunctionApplicationNode): CompileError[] { - if (field.callee && isExpressionAVariableNode(field.callee)) { - const tableGroupField = extractVarNameFromPrimaryVariable(field.callee).unwrap(); - const tableGroupFieldId = createTableGroupFieldSymbolIndex(tableGroupField); - - const tableGroupSymbol = this.symbolFactory.create(TableGroupFieldSymbol, { declaration: field }); - field.symbol = tableGroupSymbol; - - const symbolTable = this.declarationNode.symbol!.symbolTable!; - if (symbolTable.has(tableGroupFieldId)) { - const symbol = symbolTable.get(tableGroupFieldId); - return [ - new CompileError(CompileErrorCode.DUPLICATE_TABLEGROUP_FIELD_NAME, `${tableGroupField} already exists in the group`, field), - new CompileError(CompileErrorCode.DUPLICATE_TABLEGROUP_FIELD_NAME, `${tableGroupField} already exists in the group`, symbol!.declaration!), - ]; - } - symbolTable.set(tableGroupFieldId, tableGroupSymbol); - } - return []; - } -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/tablePartial.ts b/packages/dbml-parse/src/core/analyzer/validator/elementValidators/tablePartial.ts deleted file mode 100644 index 2372cfa66..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/elementValidators/tablePartial.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { partition, forIn, last } from 'lodash-es'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - AttributeNode, - BlockExpressionNode, - ElementDeclarationNode, - ExpressionNode, - FunctionApplicationNode, - FunctionExpressionNode, - ListExpressionNode, - PrimaryExpressionNode, - SyntaxNode, -} from '@/core/parser/nodes'; -import { destructureComplexVariable, extractVarNameFromPrimaryVariable } from '@/core/analyzer/utils'; -import { - aggregateSettingList, - isSimpleName, - isUnaryRelationship, - isValidColor, - isValidColumnType, - isValidDefaultValue, - isVoid, - registerSchemaStack, - pickValidator, -} from '@/core/analyzer/validator/utils'; -import { ElementValidator } from '@/core/analyzer/validator/types'; -import { ColumnSymbol, TablePartialSymbol } from '@/core/analyzer/symbol/symbols'; -import { createColumnSymbolIndex, createTablePartialSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; -import { - isExpressionAQuotedString, - isExpressionAVariableNode, - isExpressionAnIdentifierNode, -} from '@/core/parser/utils'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { ElementKind, SettingName } from '@/core/analyzer/types'; - -export default class TablePartialValidator implements ElementValidator { - private declarationNode: ElementDeclarationNode & { type: SyntaxToken }; - - private symbolFactory: SymbolFactory; - - private publicSymbolTable: SymbolTable; - - constructor ( - declarationNode: ElementDeclarationNode & { type: SyntaxToken }, - publicSymbolTable: SymbolTable, - symbolFactory: SymbolFactory, - ) { - this.declarationNode = declarationNode; - this.symbolFactory = symbolFactory; - this.publicSymbolTable = publicSymbolTable; - } - - validate (): CompileError[] { - return [ - ...this.validateContext(), - ...this.validateName(this.declarationNode.name), - ...this.validateAlias(this.declarationNode.alias), - ...this.validateSettingList(this.declarationNode.attributeList), - ...this.registerElement(), - ...this.validateBody(this.declarationNode.body), - ]; - } - - private validateContext (): CompileError[] { - if (this.declarationNode.parent instanceof ElementDeclarationNode) { - return [new CompileError( - CompileErrorCode.INVALID_TABLE_PARTIAL_CONTEXT, - 'TablePartial must appear top-level', - this.declarationNode, - )]; - } - return []; - } - - private validateName (nameNode?: SyntaxNode): CompileError[] { - if (!nameNode) { - return [new CompileError( - CompileErrorCode.NAME_NOT_FOUND, - 'A TablePartial must have a name', - this.declarationNode, - )]; - } - if (!isSimpleName(nameNode)) { - return [new CompileError( - CompileErrorCode.INVALID_NAME, - 'A TablePartial name must be an identifier or a quoted identifer', - nameNode, - )]; - } - - return []; - } - - private validateAlias (aliasNode?: SyntaxNode): CompileError[] { - if (aliasNode) { - return [new CompileError( - CompileErrorCode.UNEXPECTED_ALIAS, - 'A TablePartial shouldn\'t have an alias', - aliasNode, - )]; - } - - return []; - } - - private validateSettingList (settingList?: ListExpressionNode): CompileError[] { - const aggReport = aggregateSettingList(settingList); - const errors = aggReport.getErrors(); - const settingMap = aggReport.getValue(); - - forIn(settingMap, (attrs, name) => { - switch (name) { - case SettingName.HeaderColor: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_TABLE_PARTIAL_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isValidColor(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_PARTIAL_SETTING_VALUE, `'${name}' must be a color literal`, attr.value || attr.name!)); - } - }); - break; - case SettingName.Note: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_TABLE_PARTIAL_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isExpressionAQuotedString(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_PARTIAL_SETTING_VALUE, `'${name}' must be a string literal`, attr.value || attr.name!)); - } - }); - break; - default: - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.UNKNOWN_TABLE_PARTIAL_SETTING, `Unknown '${name}' setting`, attr))); - } - }); - return errors; - } - - registerElement (): CompileError[] { - const { name } = this.declarationNode; - this.declarationNode.symbol = this.symbolFactory.create(TablePartialSymbol, { declaration: this.declarationNode, symbolTable: new SymbolTable() }); - const maybeNamePartials = destructureComplexVariable(name); - if (!maybeNamePartials.isOk()) return []; - - const namePartials = maybeNamePartials.unwrap(); - const tablePartialName = namePartials.pop()!; - const symbolTable = registerSchemaStack(namePartials, this.publicSymbolTable, this.symbolFactory); - const tablePartialId = createTablePartialSymbolIndex(tablePartialName); - if (symbolTable.has(tablePartialId)) { - return [new CompileError(CompileErrorCode.DUPLICATE_NAME, `TablePartial name '${tablePartialName}' already exists`, name!)]; - } - symbolTable.set(tablePartialId, this.declarationNode.symbol!); - - return []; - } - - validateBody (body?: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (!body) return []; - - if (body instanceof FunctionApplicationNode) { - return [new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A TablePartial\'s body must be a block', body)]; - } - - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [ - ...this.validateFields(fields as FunctionApplicationNode[]), - ...this.validateSubElements(subs as ElementDeclarationNode[]), - ]; - } - - validateFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => { - if (!field.callee) return []; - - const errors: CompileError[] = []; - if (field.args.length === 0) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN, 'A column must have a type', field.callee!)); - } - - if (!isExpressionAVariableNode(field.callee)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_NAME, 'A column name must be an identifier or a quoted identifier', field.callee)); - } - - if (field.args[0] && !isValidColumnType(field.args[0])) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_TYPE, 'Invalid column type', field.args[0])); - } - - const remains = field.args.slice(1); - errors.push( - ...this.validateFieldSetting(remains), - ...this.registerField(field), - ); - - return errors; - }); - } - - registerField (field: FunctionApplicationNode): CompileError[] { - if (!field.callee || !isExpressionAVariableNode(field.callee)) return []; - - const columnName = extractVarNameFromPrimaryVariable(field.callee).unwrap(); - const columnId = createColumnSymbolIndex(columnName); - - const columnSymbol = this.symbolFactory.create(ColumnSymbol, { declaration: field }); - field.symbol = columnSymbol; - - const symbolTable = this.declarationNode.symbol!.symbolTable!; - if (symbolTable.has(columnId)) { - const symbol = symbolTable.get(columnId); - return [ - new CompileError(CompileErrorCode.DUPLICATE_COLUMN_NAME, `Duplicate column ${columnName}`, field), - new CompileError(CompileErrorCode.DUPLICATE_COLUMN_NAME, `Duplicate column ${columnName}`, symbol!.declaration!), - ]; - } - symbolTable.set(columnId, columnSymbol); - return []; - } - - // This is needed to support legacy inline settings - validateFieldSetting (parts: ExpressionNode[]): CompileError[] { - const lastPart = last(parts); - if ( - !parts.slice(0, -1).every(isExpressionAnIdentifierNode) - || !(isExpressionAnIdentifierNode(lastPart) || lastPart instanceof ListExpressionNode) - ) { - return [...parts.map((part) => new CompileError(CompileErrorCode.INVALID_COLUMN, 'These fields must be some inline settings optionally ended with a setting list', part))]; - } - - if (parts.length === 0) return []; - - let settingList: ListExpressionNode | undefined; - if (last(parts) instanceof ListExpressionNode) { - settingList = parts.pop() as ListExpressionNode; - } - - const aggReport = aggregateSettingList(settingList); - const errors = aggReport.getErrors(); - const settingMap: { - [index: string]: AttributeNode[]; - } & { - pk?: (AttributeNode | PrimaryExpressionNode)[]; - unique?: (AttributeNode | PrimaryExpressionNode)[]; - } = aggReport.getValue(); - - parts.forEach((part) => { - const name = extractVarNameFromPrimaryVariable(part as any).unwrap_or('').toLowerCase(); - if (name !== SettingName.PK && name !== SettingName.Unique) { - errors.push(new CompileError(CompileErrorCode.INVALID_SETTINGS, 'Inline column settings can only be `pk` or `unique`', part)); - return; - } - if (settingMap[name] === undefined) { - settingMap[name] = [part as PrimaryExpressionNode]; - } else { - settingMap[name]!.push(part as PrimaryExpressionNode); - } - }); - - const pkAttrs = settingMap[SettingName.PK] || []; - const pkeyAttrs = settingMap[SettingName.PrimaryKey] || []; - if (pkAttrs.length >= 1 && pkeyAttrs.length >= 1) { - errors.push(...[...pkAttrs, ...pkeyAttrs].map((attr) => new CompileError( - CompileErrorCode.DUPLICATE_COLUMN_SETTING, - 'Either one of \'primary key\' and \'pk\' can appear', - attr, - ))); - } - - forIn(settingMap, (attrs, name) => { - switch (name) { - case SettingName.Note: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isExpressionAQuotedString(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must be a quoted string`, attr.value || attr.name!)); - } - }); - break; - - case SettingName.Ref: - attrs.forEach((attr) => { - if (!isUnaryRelationship(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must be a valid unary relationship`, attr.value || attr.name!)); - } - }); - break; - - case SettingName.PrimaryKey: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); - } - }); - break; - - case SettingName.PK: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (attr instanceof AttributeNode && !isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); - } - }); - break; - - case SettingName.NotNull: { - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); - } - const nullAttrs = settingMap[SettingName.Null] || []; - if (attrs.length >= 1 && nullAttrs.length >= 1) { - errors.push(...[...attrs, ...nullAttrs].map((attr) => new CompileError( - CompileErrorCode.CONFLICTING_SETTING, - '\'not null\' and \'null\' can not be set at the same time', - attr, - ))); - } - attrs.forEach((attr) => { - if (!isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); - } - }); - break; - } - - case SettingName.Null: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); - } - }); - break; - - case SettingName.Unique: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (attr instanceof AttributeNode && !isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); - } - }); - break; - - case SettingName.Increment: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (attr instanceof AttributeNode && !isVoid(attr.value)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); - } - }); - break; - - case SettingName.Default: - if (attrs.length > 1) { - errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); - } - attrs.forEach((attr) => { - if (!isValidDefaultValue(attr.value)) { - errors.push(new CompileError( - CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, - `'${name}' must be a string literal, number literal, function expression, true, false or null`, - attr.value || attr.name!, - )); - } - }); - break; - - case SettingName.Check: - attrs.forEach((attr) => { - if (!(attr.value instanceof FunctionExpressionNode)) { - errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'check\' must be a function expression', attr.value || attr.name!)); - } - }); - break; - - default: - attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_COLUMN_SETTING, `Unknown column setting '${name}'`, attr))); - } - }); - return errors; - } - - private validateSubElements (subs: ElementDeclarationNode[]): CompileError[] { - const errors = subs.flatMap((sub) => { - sub.parent = this.declarationNode; - if (!sub.type) { - return []; - } - const _Validator = pickValidator(sub as ElementDeclarationNode & { type: SyntaxToken }); - const validator = new _Validator(sub as ElementDeclarationNode & { type: SyntaxToken }, this.publicSymbolTable, this.symbolFactory); - return validator.validate(); - }); - - const notes = subs.filter((sub) => sub.type?.value.toLowerCase() === ElementKind.Note); - if (notes.length > 1) { - errors.push(...notes.map((note) => new CompileError(CompileErrorCode.NOTE_REDEFINED, 'Duplicate notes are defined', note))); - } - - return errors; - } -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/types.ts b/packages/dbml-parse/src/core/analyzer/validator/types.ts deleted file mode 100644 index c477d53b7..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CompileError } from '@/core/errors'; - -export interface ElementValidator { - validate(): CompileError[]; -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/utils.ts b/packages/dbml-parse/src/core/analyzer/validator/utils.ts deleted file mode 100644 index 05ead97d4..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/utils.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { DEFAULT_SCHEMA_NAME } from '@/constants'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; -import { - AttributeNode, - BlockExpressionNode, - ElementDeclarationNode, - FunctionExpressionNode, - ListExpressionNode, - LiteralNode, - PrefixExpressionNode, - PrimaryExpressionNode, - SyntaxNode, - TupleExpressionNode, - VariableNode, - CallExpressionNode, - ArrayNode, -} from '@/core/parser/nodes'; -import { isHexChar } from '@/core/utils'; -import { destructureComplexVariable, destructureMemberAccessExpression } from '@/core/analyzer/utils'; -import CustomValidator from './elementValidators/custom'; -import EnumValidator from './elementValidators/enum'; -import IndexesValidator from './elementValidators/indexes'; -import NoteValidator from './elementValidators/note'; -import ProjectValidator from './elementValidators/project'; -import RefValidator from './elementValidators/ref'; -import TableValidator from './elementValidators/table'; -import TableGroupValidator from './elementValidators/tableGroup'; -import { createSchemaSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; -import { SchemaSymbol } from '@/core/analyzer/symbol/symbols'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { - extractStringFromIdentifierStream, isAccessExpression, isDotDelimitedIdentifier, isExpressionAQuotedString, isExpressionAVariableNode, isExpressionAnIdentifierNode, -} from '@/core/parser/utils'; -import { NUMERIC_LITERAL_PREFIX } from '@/constants'; -import Report from '@/core/report'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { ElementKind } from '@/core/analyzer/types'; -import TablePartialValidator from './elementValidators/tablePartial'; -import ChecksValidator from './elementValidators/checks'; -import RecordsValidator from './elementValidators/records'; - -export function pickValidator (element: ElementDeclarationNode & { type: SyntaxToken }) { - switch (element.type.value.toLowerCase() as ElementKind) { - case ElementKind.Enum: - return EnumValidator; - case ElementKind.Table: - return TableValidator; - case ElementKind.TableGroup: - return TableGroupValidator; - case ElementKind.Project: - return ProjectValidator; - case ElementKind.Ref: - return RefValidator; - case ElementKind.Note: - return NoteValidator; - case ElementKind.Indexes: - return IndexesValidator; - case ElementKind.TablePartial: - return TablePartialValidator; - case ElementKind.Check: - return ChecksValidator; - case ElementKind.Records: - return RecordsValidator; - default: - return CustomValidator; - } -} - -// Is the name valid (either simple or complex) -export function isValidName (nameNode: SyntaxNode): boolean { - return !!destructureComplexVariable(nameNode).unwrap_or(false); -} - -// Is the alias valid (only simple name is allowed) -export function isValidAlias ( - aliasNode: SyntaxNode, -): aliasNode is PrimaryExpressionNode & { expression: VariableNode } { - return isSimpleName(aliasNode); -} - -// Is the name valid and simple -export function isSimpleName ( - nameNode: SyntaxNode, -): nameNode is PrimaryExpressionNode & { expression: VariableNode } { - return nameNode instanceof PrimaryExpressionNode && nameNode.expression instanceof VariableNode; -} - -// Is the argument a ListExpression -export function isValidSettingList ( - settingListNode: SyntaxNode, -): settingListNode is ListExpressionNode { - return settingListNode instanceof ListExpressionNode; -} - -// Does the element has complex body -export function hasComplexBody ( - node: ElementDeclarationNode, -): node is ElementDeclarationNode & { body: BlockExpressionNode; bodyColon: undefined } { - return node.body instanceof BlockExpressionNode && !node.bodyColon; -} - -// Does the element has simple body -export function hasSimpleBody ( - node: ElementDeclarationNode, -): node is ElementDeclarationNode & { bodyColon: SyntaxToken } { - return !!node.bodyColon; -} - -export function isValidPartialInjection ( - node?: SyntaxNode, -): node is PrefixExpressionNode & { op: { value: '~' } } { - return node instanceof PrefixExpressionNode && node.op?.value === '~' && isExpressionAVariableNode(node.expression); -} -// Register the `variables` array as a stack of schema, the following nested within the former -export function registerSchemaStack ( - variables: string[], - globalSchema: SymbolTable, - symbolFactory: SymbolFactory, -): SymbolTable { - // public schema is already global schema - if (variables[0] === DEFAULT_SCHEMA_NAME) { - variables = variables.slice(1); - } - - let prevSchema = globalSchema; - - for (const curName of variables) { - let curSchema: SymbolTable | undefined; - const curId = createSchemaSymbolIndex(curName); - - if (!prevSchema.has(curId)) { - curSchema = new SymbolTable(); - const curSymbol = symbolFactory.create(SchemaSymbol, { symbolTable: curSchema }); - prevSchema.set(curId, curSymbol); - } else { - curSchema = prevSchema.get(curId)?.symbolTable; - if (!curSchema) { - throw new Error('Expect a symbol table in a schema symbol'); - } - } - prevSchema = curSchema; - } - - return prevSchema; -} - -export function isRelationshipOp (op?: string): boolean { - return op === '-' || op === '<>' || op === '>' || op === '<'; -} - -export function isValidColor (value?: SyntaxNode): boolean { - if ( - !(value instanceof PrimaryExpressionNode) - || !(value.expression instanceof LiteralNode) - || !(value.expression.literal?.kind === SyntaxTokenKind.COLOR_LITERAL) - ) { - return false; - } - - const color = value.expression.literal.value; - - // e.g. #fff or #0abcde - if (color.length !== 4 && color.length !== 7) { - return false; - } - - if (color[0] !== '#') { - return false; - } - - for (let i = 1; i < color.length; i += 1) { - if (!isHexChar(color[i])) { - return false; - } - } - - return true; -} - -// Is the value non-existent -export function isVoid (value?: SyntaxNode): boolean { - return value === undefined; -} - -// Is the `value` a valid value for a column's `default` setting -// It's a valid only if it's a literal or a complex variable (potentially an enum member) -export function isValidDefaultValue (value?: SyntaxNode): boolean { - if (isExpressionAQuotedString(value)) { - return true; - } - - if (isExpressionASignedNumberExpression(value)) { - return true; - } - - if (isExpressionAnIdentifierNode(value) && ['true', 'false', 'null'].includes(value.expression.variable.value.toLowerCase())) { - return true; - } - - if ( - value instanceof PrefixExpressionNode - && NUMERIC_LITERAL_PREFIX.includes(value.op?.value as any) - && isExpressionASignedNumberExpression(value.expression) - ) { - return true; - } - - if (value instanceof FunctionExpressionNode) { - return true; - } - - if (!value) return false; - if (!isDotDelimitedIdentifier(value)) return false; - const fragments = destructureMemberAccessExpression(value).unwrap(); - return fragments.length === 2 || fragments.length === 3; -} - -export type SignedNumberExpression = - (PrimaryExpressionNode & { expression: LiteralNode & { literal: { kind: SyntaxTokenKind.NUMERIC_LITERAL } } }) - | (PrefixExpressionNode & { op: '-' | '+'; expression: SignedNumberExpression }); -export function isExpressionASignedNumberExpression (value?: SyntaxNode): value is SignedNumberExpression { - if (value instanceof PrefixExpressionNode) { - if (!NUMERIC_LITERAL_PREFIX.includes(value.op!.value)) return false; - return isExpressionASignedNumberExpression(value.expression); - } - return ( - value instanceof PrimaryExpressionNode - && value.expression instanceof LiteralNode - && value.expression.literal?.kind === SyntaxTokenKind.NUMERIC_LITERAL - ); -} - -export function isUnaryRelationship (value?: SyntaxNode): value is PrefixExpressionNode { - if (!(value instanceof PrefixExpressionNode)) { - return false; - } - - if (!isRelationshipOp(value.op?.value)) { - return false; - } - - const variables = destructureComplexVariable(value.expression).unwrap_or(undefined); - - return variables !== undefined && variables.length > 0; -} - -export function isTupleOfVariables (value?: SyntaxNode): value is TupleExpressionNode & { - elementList: (PrimaryExpressionNode & { expression: VariableNode })[]; -} { - return value instanceof TupleExpressionNode && value.elementList.every(isExpressionAVariableNode); -} - -export function isValidColumnType (type: SyntaxNode): boolean { - if ( - !( - type instanceof CallExpressionNode - || isAccessExpression(type) - || type instanceof PrimaryExpressionNode - || type instanceof ArrayNode - ) - ) { - return false; - } - - while (type instanceof CallExpressionNode || type instanceof ArrayNode) { - if (type instanceof CallExpressionNode) { - if (type.callee === undefined || type.argumentList === undefined) { - return false; - } - - if (!type.argumentList.elementList.every((e) => isExpressionASignedNumberExpression(e) || isExpressionAQuotedString(e) || isExpressionAnIdentifierNode(e))) { - return false; - } - - type = type.callee; - } else if (type instanceof ArrayNode) { - if (type.array === undefined || type.indexer === undefined) { - return false; - } - - if (!type.indexer.elementList.every((attribute) => !attribute.colon && !attribute.value && isExpressionASignedNumberExpression(attribute.name))) { - return false; - } - - type = type.array; - } - } - - const variables = destructureComplexVariable(type).unwrap_or(undefined); - - return variables !== undefined && variables.length > 0; -} - -export function aggregateSettingList (settingList?: ListExpressionNode): Report<{ [index: string]: AttributeNode[] }> { - const map: { [index: string]: AttributeNode[] } = {}; - const errors: CompileError[] = []; - if (!settingList) { - return new Report({}); - } - settingList.elementList.forEach((attribute) => { - if (!attribute.name) return; - - if (attribute.name instanceof PrimaryExpressionNode) { - errors.push(new CompileError(CompileErrorCode.INVALID_SETTINGS, 'A setting name must be a stream of identifiers', attribute.name)); - return; - } - - const name = extractStringFromIdentifierStream(attribute.name).unwrap_or(undefined)?.toLowerCase(); - if (!name) return; - - if (map[name] === undefined) { - map[name] = [attribute]; - } else { - map[name].push(attribute); - } - }); - - return new Report(map, errors); -} diff --git a/packages/dbml-parse/src/core/analyzer/validator/validator.ts b/packages/dbml-parse/src/core/analyzer/validator/validator.ts deleted file mode 100644 index 93c8e8816..000000000 --- a/packages/dbml-parse/src/core/analyzer/validator/validator.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Report from '@/core/report'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { ElementDeclarationNode, ProgramNode } from '@/core/parser/nodes'; -import { SchemaSymbol } from '@/core/analyzer/symbol/symbols'; -import SymbolFactory from '@/core/analyzer/symbol/factory'; -import { pickValidator } from '@/core/analyzer/validator/utils'; -import SymbolTable from '@/core/analyzer/symbol/symbolTable'; -import { SyntaxToken } from '@/core/lexer/tokens'; -import { getElementKind } from '@/core/analyzer/utils'; -import { ElementKind } from '@/core/analyzer/types'; - -export default class Validator { - private ast: ProgramNode; - - private publicSchemaSymbol: SchemaSymbol; - - private symbolFactory: SymbolFactory; - - constructor (ast: ProgramNode, symbolFactory: SymbolFactory) { - this.ast = ast; - this.symbolFactory = symbolFactory; - this.publicSchemaSymbol = this.symbolFactory.create(SchemaSymbol, { - symbolTable: new SymbolTable(), - }); - - this.ast.symbol = this.publicSchemaSymbol; - this.ast.symbol.declaration = this.ast; - } - - validate (): Report { - const errors: CompileError[] = []; - - this.ast.body.forEach((element) => { - element.parent = this.ast; - if (element.type === undefined) { - return; - } - - const Val = pickValidator(element as ElementDeclarationNode & { type: SyntaxToken }); - const validatorObject = new Val( - element as ElementDeclarationNode & { type: SyntaxToken }, - this.publicSchemaSymbol.symbolTable, - this.symbolFactory, - ); - errors.push(...validatorObject.validate()); - }); - - const projects = this.ast.body.filter((e) => getElementKind(e).unwrap_or(undefined) === ElementKind.Project); - if (projects.length > 1) { - projects.forEach((project) => errors.push(new CompileError(CompileErrorCode.PROJECT_REDEFINED, 'Only one project can exist', project))); - } - - return new Report(this.ast, errors); - } -} diff --git a/packages/dbml-parse/src/core/global_modules/checks/bind.ts b/packages/dbml-parse/src/core/global_modules/checks/bind.ts new file mode 100644 index 000000000..8ed1b611f --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/checks/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindChecks (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Checks)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/checks/index.ts b/packages/dbml-parse/src/core/global_modules/checks/index.ts new file mode 100644 index 000000000..5266a91ee --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/checks/index.ts @@ -0,0 +1,43 @@ +import { isElementNode } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import type { GlobalModule } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { interpretChecks } from './interpret'; +import { bindChecks } from './bind'; + +export const checksModule: GlobalModule = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Checks)) { + return Report.create(PASS_THROUGH); + } + return new Report(new NodeSymbol({ + kind: SymbolKind.Checks, + declaration: node, + })); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (!symbol.isKind(SymbolKind.Checks)) { + return Report.create(PASS_THROUGH); + } + return new Report([]); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Checks)) { + return Report.create(PASS_THROUGH); + } + return new Report([]); + }, + + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { return Report.create(PASS_THROUGH); }, + + bind: bindChecks, + + interpret: interpretChecks, +}; diff --git a/packages/dbml-parse/src/core/global_modules/checks/interpret.ts b/packages/dbml-parse/src/core/global_modules/checks/interpret.ts new file mode 100644 index 000000000..b1b7944ee --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/checks/interpret.ts @@ -0,0 +1,50 @@ +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { + BlockExpressionNode, + ElementDeclarationNode, + FunctionApplicationNode, + FunctionExpressionNode, + ListExpressionNode, +} from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { getTokenPosition } from '../utils'; +import { Check, schemaFrom } from '@/core/types/schemaJson'; +import { extractQuotedStringToken } from '@/core/syntax/utils'; +import { isElementNode } from '@/core/utils/expression'; +import { collectSettings } from '@/core/utils/validate'; + +// Returns an array of Check elements, one per backtick expression. +export function interpretChecks (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Checks)) return Report.create(PASS_THROUGH); + if (!(node instanceof ElementDeclarationNode)) return new Report(undefined); + + const body = node.body; + if (!(body instanceof BlockExpressionNode)) return new Report([]); + + const checks = body.body.flatMap((field) => { + if (!(field instanceof FunctionApplicationNode)) return []; + + const token = getTokenPosition(field); + + // Extract the backtick expression as the check body + const expression = field.callee instanceof FunctionExpressionNode + ? (field.callee.value?.value ?? '') + : ''; + + // Extract optional name from settings (e.g. [name: 'check_name']) + let name: string | undefined; + const settingsList = field.args.find((a): a is ListExpressionNode => a instanceof ListExpressionNode); + if (settingsList) { + const settingsReport = collectSettings(settingsList); + const settingMap = settingsReport.getValue(); + name = extractQuotedStringToken(settingMap.get(SettingName.Name)?.at(0)?.value); + } + + return [schemaFrom(Check, { expression, token, ...(name !== undefined && { name }) })]; + }); + + return new Report(checks); +} diff --git a/packages/dbml-parse/src/core/global_modules/checks/utils.ts b/packages/dbml-parse/src/core/global_modules/checks/utils.ts new file mode 100644 index 000000000..70ca3651a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/checks/utils.ts @@ -0,0 +1 @@ +// Module-specific utilities diff --git a/packages/dbml-parse/src/core/global_modules/enum/bind.ts b/packages/dbml-parse/src/core/global_modules/enum/bind.ts new file mode 100644 index 000000000..62f8299a9 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/enum/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindEnum (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Enum) && !isElementFieldNode(node, ElementKind.Enum)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/enum/index.ts b/packages/dbml-parse/src/core/global_modules/enum/index.ts new file mode 100644 index 000000000..2faa74e5b --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/enum/index.ts @@ -0,0 +1,63 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import type { SchemaElement } from '@/core/types/schemaJson'; +import { getNodeMemberSymbols, getSymbolDirectMembers } from '../utils'; +import { interpretEnum, interpretEnumField } from './interpret'; +import { bindEnum } from './bind'; + +export const enumModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Enum)) { + return new Report(new NodeSymbol({ + kind: SymbolKind.Enum, + declaration: node, + })); + } + if (isElementFieldNode(node, ElementKind.Enum)) { + return new Report(new NodeSymbol({ + kind: SymbolKind.EnumField, + declaration: node, + })); + } + return Report.create(PASS_THROUGH); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (!symbol.isKind(SymbolKind.Enum)) { + return Report.create(PASS_THROUGH); + } + + return getSymbolDirectMembers(compiler, symbol); + }, + + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { return Report.create(PASS_THROUGH); }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Enum)) { + return getNodeMemberSymbols(compiler, node); + } + if (isElementFieldNode(node, ElementKind.Enum)) { + return new Report([]); + } + return Report.create(PASS_THROUGH); + }, + + bind: bindEnum, + + interpret (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Enum)) { + return interpretEnum(compiler, node); + } + if (isElementFieldNode(node, ElementKind.Enum)) { + return interpretEnumField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/global_modules/enum/interpret.ts b/packages/dbml-parse/src/core/global_modules/enum/interpret.ts new file mode 100644 index 000000000..c9174b1b9 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/enum/interpret.ts @@ -0,0 +1,90 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { ElementKind, SettingName } from '@/core/types/keywords'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { extractQuotedStringToken } from '@/core/syntax/utils'; +import { getTokenPosition, normalizeNoteContent } from '../utils'; +import { EnumField, Enum, schemaFrom } from '@/core/types/schemaJson'; +import type { TokenPosition } from '@/core/types/schemaJson'; +import type { CompileError } from '@/core/types/errors'; + +export function interpretEnum (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Enum)) return Report.create(PASS_THROUGH); + + // Derive schema-qualified enum name + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const lastSegment = name?.at(-1) ?? ''; + const schemaSegments = name?.slice(0, -1) ?? []; + + const token = getTokenPosition(node); + + // Fall back to an empty enum if symbol resolution fails + const symbolResult = compiler.nodeSymbol(node); + if (symbolResult.hasValue(UNHANDLED)) { + return new Report(schemaFrom(Enum, { + name: lastSegment, + schemaName: schemaSegments.join('.') || null, + values: [], + token, + })); + } + + // Recursively interpret each enum value member + const membersResult = compiler.symbolMembers(symbolResult.getValue()); + const members = !membersResult.hasValue(UNHANDLED) ? membersResult.getValue() : []; + + const values: EnumField[] = []; + const errors: CompileError[] = []; + for (const member of members) { + if (!member.declaration) continue; + const fieldResult = compiler.interpret(member.declaration); + if (!fieldResult.hasValue(UNHANDLED)) { + const value = fieldResult.getValue(); + if (value instanceof EnumField) values.push(value); + errors.push(...fieldResult.getErrors()); + } + } + + return new Report(schemaFrom(Enum, { + name: lastSegment, + schemaName: schemaSegments.join('.') || null, + values, + token, + }), errors); +} + +export function interpretEnumField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.Enum)) return Report.create(PASS_THROUGH); + + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const fieldName = name?.at(-1) ?? ''; + + const token = getTokenPosition(node); + + // Extract optional note from field-level settings + const settingsResult = compiler.settings(node); + const errors: CompileError[] = [...settingsResult.getErrors()]; + + let note: { value: string; token: TokenPosition } | undefined; + if (!settingsResult.hasValue(UNHANDLED)) { + const settings = settingsResult.getValue(); + if (settings.get(SettingName.Note)?.[0]?.value) { + const noteValue = extractQuotedStringToken(settings.get(SettingName.Note)?.[0].value); + if (noteValue !== undefined) { + note = { + value: normalizeNoteContent(noteValue), + token: getTokenPosition(settings.get(SettingName.Note)![0]), + }; + } + } + } + + const result = new EnumField(token, fieldName); + if (note) result.note = note; + + return new Report(result, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/enum/utils.ts b/packages/dbml-parse/src/core/global_modules/enum/utils.ts new file mode 100644 index 000000000..70ca3651a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/enum/utils.ts @@ -0,0 +1 @@ +// Module-specific utilities diff --git a/packages/dbml-parse/src/core/global_modules/index.ts b/packages/dbml-parse/src/core/global_modules/index.ts new file mode 100644 index 000000000..54685feb4 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/index.ts @@ -0,0 +1,85 @@ +import type { GlobalModule } from './types'; +import { PASS_THROUGH, UNHANDLED, type PassThrough } from '@/constants'; +import { tableModule } from './table'; +import { refModule } from './ref'; +import { projectModule } from './project'; +import { tableGroupModule } from './tableGroup'; +import { tablePartialModule } from './tablePartial'; +import { noteModule } from './stickyNote'; +import { enumModule } from './enum'; +import { recordsModule } from './records'; +import { indexesModule } from './indexes'; +import { checksModule } from './checks'; +import { programModule } from './program'; +import { schemaModule } from './schema'; +import type Compiler from '@/compiler/index'; +import type { SyntaxNode } from '@/core/types/nodes'; +import Report from '@/core/types/report'; +import type { NodeSymbol } from '@/core/types/symbols'; +import type { SchemaElement } from '@/core/types/schemaJson'; +import type { Unhandled } from '@/constants'; + +// Registry of all element modules; the dispatcher tries each in order until one claims the node. +// Each time you add a new element, register its module here. +export const modules: GlobalModule[] = [ + tableModule, + enumModule, + recordsModule, + indexesModule, + checksModule, + refModule, + projectModule, + tableGroupModule, + tablePartialModule, + noteModule, + schemaModule, + programModule, +]; + +// Chain-of-responsibility: iterate modules until one handles the node (returns non-PASS_THROUGH) +function dispatch ( + method: K, + ...args: Parameters> +): ReturnType> | Report { + for (const module of modules) { + const fn = module[method] as any; + if (fn) { + const result = fn(...args); + if (!result.hasValue(PASS_THROUGH)) { + return result; + } + } + } + + return Report.create(PASS_THROUGH); +} + +export function nodeSymbol (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('nodeSymbol', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} + +export function symbolMembers (this: Compiler, symbol: NodeSymbol): Report | Report { + const res = dispatch('symbolMembers', this, symbol); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} + +export function nestedSymbols (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('nestedSymbols', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} + +export function nodeReferee (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('nodeReferee', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} + +export function bind (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('bind', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} + +export function interpret (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('interpret', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} diff --git a/packages/dbml-parse/src/core/global_modules/indexes/bind.ts b/packages/dbml-parse/src/core/global_modules/indexes/bind.ts new file mode 100644 index 000000000..ba8b97b5d --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/indexes/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindIndexes (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Indexes) && !isElementFieldNode(node, ElementKind.Indexes)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/indexes/index.ts b/packages/dbml-parse/src/core/global_modules/indexes/index.ts new file mode 100644 index 000000000..c056b69d5 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/indexes/index.ts @@ -0,0 +1,80 @@ +import { isElementNode, isElementFieldNode, isExpressionAVariableNode, isInsideSettingList } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import { ElementDeclarationNode, PrimaryExpressionNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import type { SchemaElement } from '@/core/types/schemaJson'; +import { getNodeMemberSymbols, lookupMember } from '../utils'; +import { interpretIndexes } from './interpret'; +import { bindIndexes } from './bind'; + +export const indexesModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Indexes)) { + return new Report(new NodeSymbol({ + kind: SymbolKind.Indexes, + declaration: node, + })); + } + if (isElementFieldNode(node, ElementKind.Indexes)) { + if (node instanceof PrimaryExpressionNode) { + return new Report(new NodeSymbol({ kind: SymbolKind.IndexesField, declaration: node })); + } + return Report.create(PASS_THROUGH); + } + return Report.create(PASS_THROUGH); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (symbol.isKind(SymbolKind.Indexes)) { + if (!symbol.declaration) { + return new Report([]); + } + const symbols = compiler.nestedSymbols(symbol.declaration); + if (symbols.hasValue(UNHANDLED)) { + return new Report([]); + } + return symbols as Report; + } + if (symbol.isKind(SymbolKind.IndexesField)) { + return new Report([]); + } + return Report.create(PASS_THROUGH); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Indexes)) { + return getNodeMemberSymbols(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isExpressionAVariableNode(node)) return Report.create(PASS_THROUGH); + // Skip variables inside index settings (e.g. [type: btree]) + if (isInsideSettingList(node)) return Report.create(PASS_THROUGH); + let ancestor: SyntaxNode | undefined = node; + while (ancestor && !(ancestor instanceof ElementDeclarationNode && ancestor.isKind(ElementKind.Indexes))) ancestor = ancestor.parent; + if (!ancestor) return Report.create(PASS_THROUGH); + const tableNode = ancestor.parentElement; + if (!tableNode || !isElementNode(tableNode, ElementKind.Table)) return Report.create(PASS_THROUGH); + const tableSymbol = compiler.nodeSymbol(tableNode); + if (tableSymbol.hasValue(UNHANDLED)) return new Report(undefined); + const varName = isExpressionAVariableNode(node) ? (node.expression.variable?.value ?? '') : ''; + return lookupMember(compiler, tableSymbol.getValue(), varName, { kinds: [SymbolKind.Column] }); + }, + + bind: bindIndexes, + + interpret (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Indexes)) { + return interpretIndexes(compiler, node); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/global_modules/indexes/interpret.ts b/packages/dbml-parse/src/core/global_modules/indexes/interpret.ts new file mode 100644 index 000000000..353c20bb2 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/indexes/interpret.ts @@ -0,0 +1,111 @@ +import { ElementKind } from '@/core/types/keywords'; +import { + BlockExpressionNode, + CallExpressionNode, + ElementDeclarationNode, + FunctionApplicationNode, + ListExpressionNode, +} from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { getTokenPosition } from '../utils'; +import { Index } from '@/core/types/schemaJson'; +import type { TokenPosition } from '@/core/types/schemaJson'; +import { extractQuotedStringToken, extractVarNameFromPrimaryVariable, destructureIndexNode } from '@/core/syntax/utils'; +import { extractVariableFromExpression } from '@/core/syntax/utils'; +import { SettingName } from '@/core/types/keywords'; +import { isElementNode } from '@/core/utils/expression'; +import { last } from 'lodash-es'; + +// Returns an array of Index elements, one per index entry in the block. +export function interpretIndexes (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Indexes)) return Report.create(PASS_THROUGH); + if (!(node instanceof ElementDeclarationNode)) return new Report(undefined); + + const body = node.body; + if (!(body instanceof BlockExpressionNode)) return new Report([]); + + const indexes = body.body.flatMap((field) => { + if (!(field instanceof FunctionApplicationNode)) return []; + if (!field.callee) return []; + + const columns: Index['columns'] = []; + const token = getTokenPosition(field); + const args: SyntaxNode[] = [field.callee, ...field.args]; + + // Extract settings from trailing list expression + let pk: boolean | undefined; + let unique: boolean | undefined; + let name: string | undefined; + let note: { value: string; token: TokenPosition } | undefined; + let type: string | undefined; + + // Pop trailing ListExpressionNode so it doesn't pollute column parsing + if (last(args) instanceof ListExpressionNode) { + args.pop(); + } + + const settingsResult = compiler.settings(field); + if (!settingsResult.hasValue(UNHANDLED)) { + const settingMap = settingsResult.getValue(); + pk = !!settingMap.get(SettingName.PK)?.length; + unique = !!settingMap.get(SettingName.Unique)?.length; + name = extractQuotedStringToken(settingMap.get(SettingName.Name)?.at(0)?.value); + const noteNode = settingMap.get(SettingName.Note)?.at(0); + if (noteNode) { + const noteValue = extractQuotedStringToken(noteNode.value); + if (noteValue !== undefined) { + note = { value: noteValue, token: getTokenPosition(noteNode) }; + } + } + type = extractVariableFromExpression(settingMap.get(SettingName.Type)?.at(0)?.value); + } + + // Flatten call expressions like (id, name)(age, weight) into individual column refs + const flatArgs = args.flatMap((arg) => { + if (!(arg instanceof CallExpressionNode)) return arg; + const fragments: SyntaxNode[] = []; + let current: SyntaxNode = arg; + while (current instanceof CallExpressionNode) { + if (current.argumentList) fragments.push(current.argumentList); + if (!current.callee) break; + current = current.callee; + } + fragments.push(current); + return fragments; + }); + + // Parse each arg into index column entries (functional or non-functional) + for (const arg of flatArgs) { + const result = destructureIndexNode(arg); + if (!result) continue; + for (const s of result.functional) { + columns.push({ + value: s.value?.value ?? '', + type: 'expression', + token: getTokenPosition(s), + }); + } + for (const s of result.nonFunctional) { + columns.push({ + value: extractVarNameFromPrimaryVariable(s) ?? '', + type: 'column', + token: getTokenPosition(s), + }); + } + } + + const idx = new Index(token); + idx.columns = columns; + if (pk !== undefined) idx.pk = pk; + if (unique !== undefined) idx.unique = unique; + if (name) idx.name = name; + if (note) idx.note = note; + if (type) idx.type = type; + return [idx]; + }); + + return new Report(indexes); +} diff --git a/packages/dbml-parse/src/core/global_modules/indexes/utils.ts b/packages/dbml-parse/src/core/global_modules/indexes/utils.ts new file mode 100644 index 000000000..70ca3651a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/indexes/utils.ts @@ -0,0 +1 @@ +// Module-specific utilities diff --git a/packages/dbml-parse/src/core/global_modules/program/bind.ts b/packages/dbml-parse/src/core/global_modules/program/bind.ts new file mode 100644 index 000000000..358dab323 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/program/bind.ts @@ -0,0 +1,248 @@ +import { ElementDeclarationNode, FunctionApplicationNode, PrefixExpressionNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { getBody, isElementNode, isExpressionAVariableNode, isProgramNode, isRelationshipOp } from '@/core/utils/expression'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { bindNode, bindDeep, validateNode } from '../utils/bind'; +import { isBinaryRelationship, destructureComplexVariableTuple, extractVariableFromExpression } from '@/core/syntax/utils'; +import { ElementKind, SettingName } from '@/core/types/keywords'; + +interface RefEndpointInfo { + schemaName: string | null; + tableName: string; + fieldNames: string[]; + node: SyntaxNode; +} + +function extractRefEndpointsFromBinaryExpr (expr: SyntaxNode): { endpoints: [RefEndpointInfo, RefEndpointInfo]; op: string } | undefined { + if (!isBinaryRelationship(expr)) return undefined; + const leftTuple = destructureComplexVariableTuple(expr.leftExpression); + const rightTuple = destructureComplexVariableTuple(expr.rightExpression); + if (!leftTuple || !rightTuple) return undefined; + + const buildEP = (tuple: NonNullable>): RefEndpointInfo => { + const vars = tuple.variables.map((v) => v.expression.variable?.value ?? ''); + if (tuple.tupleElements.length > 0) { + return { tableName: vars.at(-1) ?? '', schemaName: vars.length > 1 ? vars.slice(0, -1).join('.') : null, fieldNames: tuple.tupleElements.map((e) => e.expression.variable?.value ?? ''), node: expr }; + } + return { fieldNames: vars.length > 0 ? [vars.at(-1)!] : [], tableName: vars.length > 1 ? vars.at(-2)! : '', schemaName: vars.length > 2 ? vars.slice(0, -2).join('.') : null, node: expr }; + }; + + return { endpoints: [buildEP(leftTuple), buildEP(rightTuple)], op: expr.op?.value ?? '' }; +} + +function extractInlineRefEndpoints (compiler: Compiler, tableNode: ElementDeclarationNode): { tableName: string; schemaName: string | null; endpoints: [RefEndpointInfo, RefEndpointInfo]; op: string; node: SyntaxNode }[] { + const results: { tableName: string; schemaName: string | null; endpoints: [RefEndpointInfo, RefEndpointInfo]; op: string; node: SyntaxNode }[] = []; + const fn = compiler.fullname(tableNode); + if (fn.hasValue(UNHANDLED)) return results; + const fullname = fn.getValue(); + const tableName = fullname?.at(-1) ?? ''; + const schemaName = fullname && fullname.length > 1 ? fullname.slice(0, -1).join('.') : null; + + const body = getBody(tableNode); + for (const field of body) { + if (!(field instanceof FunctionApplicationNode)) continue; + const fieldName = extractVariableFromExpression(field.callee) ?? ''; + const settingsResult = compiler.settings(field); + if (settingsResult.hasValue(UNHANDLED)) continue; + const settings = settingsResult.getValue(); + const refAttrs = settings.get(SettingName.Ref); + if (!refAttrs) continue; + for (const refAttr of refAttrs) { + if (!refAttr.value) continue; + let op: string | undefined; + let targetTuple: ReturnType; + + if (refAttr.value instanceof PrefixExpressionNode && isRelationshipOp(refAttr.value.op?.value)) { + op = refAttr.value.op!.value; + targetTuple = destructureComplexVariableTuple(refAttr.value.expression); + } else if (isBinaryRelationship(refAttr.value)) { + op = refAttr.value.op?.value; + targetTuple = destructureComplexVariableTuple(refAttr.value.rightExpression); + } + if (!op || !targetTuple) continue; + const vars = targetTuple.variables.map((v) => v.expression.variable?.value ?? ''); + let targetTableName: string; + let targetSchemaName: string | null; + let targetFieldNames: string[]; + if (targetTuple.tupleElements.length > 0) { + targetTableName = vars.at(-1) ?? ''; + targetSchemaName = vars.length > 1 ? vars.slice(0, -1).join('.') : null; + targetFieldNames = targetTuple.tupleElements.map((e) => e.expression.variable?.value ?? ''); + } else { + targetFieldNames = vars.length > 0 ? [vars.at(-1)!] : []; + targetTableName = vars.length > 1 ? vars.at(-2)! : ''; + targetSchemaName = vars.length > 2 ? vars.slice(0, -2).join('.') : null; + } + // Default to containing table when target has no table qualifier + const effectiveTargetTable = targetTableName || tableName; + const effectiveTargetSchema = targetTableName ? targetSchemaName : schemaName; + const leftEP: RefEndpointInfo = { tableName, schemaName, fieldNames: [fieldName], node: refAttr }; + const rightEP: RefEndpointInfo = { tableName: effectiveTargetTable, schemaName: effectiveTargetSchema, fieldNames: targetFieldNames, node: refAttr }; + results.push({ tableName, schemaName, endpoints: [leftEP, rightEP], op: op!, node: refAttr }); + } + } + return results; +} + +function makeRefKey (ep0: RefEndpointInfo, ep1: RefEndpointInfo, op?: string): string { + const k0 = `${ep0.schemaName ?? ''}.${ep0.tableName}.(${ep0.fieldNames.join(',')})`; + const k1 = `${ep1.schemaName ?? ''}.${ep1.tableName}.(${ep1.fieldNames.join(',')})`; + if (!op) return [k0, k1].sort().join(' <> '); + // Normalize direction so the same relationship always produces the same key: + // `A > B` (A references B) is the same relationship as `B < A` + // `A - B` is symmetric with `B - A` + // `A <> B` is symmetric with `B <> A` + if (op === '<') { + // Flip: left < right => right > left (canonical form uses >) + return `${k1} > ${k0}`; + } + if (op === '-' || op === '<>') { + // Symmetric: sort endpoints + return [k0, k1].sort().join(` ${op} `); + } + // '>' : keep as-is (canonical form) + return `${k0} > ${k1}`; +} + +export function bindProgram (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isProgramNode(node)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + // Post-bind validation: detect circular refs and same-endpoint refs + const allRefPairs: { key: string; node: SyntaxNode }[] = []; + const body = node.body; + for (const child of body) { + if (!(child instanceof ElementDeclarationNode)) continue; + // Standalone Ref elements - only track for circular ref detection, no same-endpoint check + if (isElementNode(child, ElementKind.Ref)) { + const bodyFields = getBody(child); + for (const field of bodyFields) { + const expr = field instanceof FunctionApplicationNode ? field.callee : undefined; + if (!expr) continue; + const result = extractRefEndpointsFromBinaryExpr(expr); + if (result) { + allRefPairs.push({ key: makeRefKey(result.endpoints[0], result.endpoints[1], result.op), node: child }); + } + } + } + // Table elements: detect both same-endpoint and add to circular ref pool + if (isElementNode(child, ElementKind.Table)) { + const inlineRefs = extractInlineRefEndpoints(compiler, child); + for (const ir of inlineRefs) { + allRefPairs.push({ key: makeRefKey(ir.endpoints[0], ir.endpoints[1], ir.op), node: ir.node }); + // Check same endpoint + const k0 = `${ir.endpoints[0].schemaName ?? ''}.${ir.endpoints[0].tableName}.(${ir.endpoints[0].fieldNames.join(',')})`; + const k1 = `${ir.endpoints[1].schemaName ?? ''}.${ir.endpoints[1].tableName}.(${ir.endpoints[1].fieldNames.join(',')})`; + if (k0 === k1) { + errors.push(new CompileError(CompileErrorCode.SAME_ENDPOINT, 'Two endpoints are the same', ir.node)); + } + } + } + // TablePartial: only detect same-endpoint when the ref target is a standalone variable + // (e.g., `ref: > col` means the same column, but `ref: > T.col` could refer to another table) + if (isElementNode(child, ElementKind.TablePartial)) { + const body2 = getBody(child); + for (const field of body2) { + if (!(field instanceof FunctionApplicationNode)) continue; + const fieldNameVal = extractVariableFromExpression(field.callee) ?? ''; + const settingsResult = compiler.settings(field); + if (settingsResult.hasValue(UNHANDLED)) continue; + const settings = settingsResult.getValue(); + const refAttrs = settings.get(SettingName.Ref); + if (!refAttrs) continue; + for (const refAttr of refAttrs) { + if (!refAttr.value) continue; + // Only handle prefix form with standalone variable target + if (refAttr.value instanceof PrefixExpressionNode && isRelationshipOp(refAttr.value.op?.value)) { + const target = refAttr.value.expression; + if (isExpressionAVariableNode(target)) { + const targetField = target.expression.variable?.value ?? ''; + if (targetField === fieldNameVal) { + errors.push(new CompileError(CompileErrorCode.SAME_ENDPOINT, 'Two endpoints are the same', refAttr)); + } + } + } + } + } + } + } + + // Detect circular refs (same endpoints across different refs) + const seen = new Map(); + for (const pair of allRefPairs) { + const existing = seen.get(pair.key); + if (existing) { + errors.push(new CompileError(CompileErrorCode.CIRCULAR_REF, 'References with same endpoints exist', pair.node)); + errors.push(new CompileError(CompileErrorCode.CIRCULAR_REF, 'References with same endpoints exist', existing)); + } else { + seen.set(pair.key, pair.node); + } + } + + // Detect tables appearing in multiple table groups + { + // Build alias -> table name map + const aliasToTable = new Map(); + for (const child of node.body) { + if (!(child instanceof ElementDeclarationNode)) continue; + if (!isElementNode(child, ElementKind.Table)) continue; + const fn = compiler.fullname(child); + if (fn.hasValue(UNHANDLED)) continue; + const fullname = fn.getValue(); + const tableName = fullname?.join('.') ?? ''; + const aliasResult = compiler.alias(child); + if (!aliasResult.hasValue(UNHANDLED) && aliasResult.getValue()) { + aliasToTable.set(aliasResult.getValue()!, tableName); + } + } + + // Scan all table groups for entries + const tableToGroups = new Map(); + for (const child of node.body) { + if (!(child instanceof ElementDeclarationNode)) continue; + if (!isElementNode(child, ElementKind.TableGroup)) continue; + const groupFn = compiler.fullname(child); + const groupName = groupFn.hasValue(UNHANDLED) ? '' : (groupFn.getValue()?.at(-1) ?? ''); + + const groupBody = getBody(child); + for (const field of groupBody) { + if (!(field instanceof FunctionApplicationNode)) continue; + const fieldFn = compiler.fullname(field); + if (fieldFn.hasValue(UNHANDLED)) continue; + const fieldName = fieldFn.getValue()?.join('.') ?? ''; + // Resolve alias to canonical table name + const canonicalName = aliasToTable.get(fieldName) ?? fieldName; + + if (!tableToGroups.has(canonicalName)) { + tableToGroups.set(canonicalName, []); + } + tableToGroups.get(canonicalName)!.push({ groupName, node: field }); + } + } + + // Report errors for tables appearing in multiple groups + for (const [, groups] of tableToGroups) { + if (groups.length <= 1) continue; + for (let i = 1; i < groups.length; i++) { + const fieldFn = compiler.fullname(groups[i].node); + const displayName = fieldFn.hasValue(UNHANDLED) ? '' : (fieldFn.getValue()?.join('.') ?? ''); + const firstGroupName = groups[0].groupName; + errors.push(new CompileError( + CompileErrorCode.TABLE_REAPPEAR_IN_TABLEGROUP, + `Table "${displayName}" already appears in group "${firstGroupName}"`, + groups[i].node, + )); + } + } + } + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/program/index.ts b/packages/dbml-parse/src/core/global_modules/program/index.ts new file mode 100644 index 000000000..9d35c05fc --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/program/index.ts @@ -0,0 +1,68 @@ +import { isProgramNode } from '@/core/utils/expression'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SchemaSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { DEFAULT_SCHEMA_NAME, PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { getNodeMemberSymbols, getSymbolDirectMembers } from '../utils'; +import { bindProgram } from './bind'; +import { interpretProgram } from './interpret'; + +export const programModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isProgramNode(node)) { + return Report.create(PASS_THROUGH); + } + + return new Report(new NodeSymbol({ + kind: SymbolKind.Program, + declaration: node, + })); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isProgramNode(node)) { + return Report.create(PASS_THROUGH); + } + return getNodeMemberSymbols(compiler, node); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (!symbol.isKind(SymbolKind.Program)) { + return Report.create(PASS_THROUGH); + } + + const directMembers = getSymbolDirectMembers(compiler, symbol); + const members = directMembers.getValue(); + const errors = [...directMembers.getErrors()]; + const warnings = [...directMembers.getWarnings()]; + + // Synthesize SchemaSymbol instances from qualified element names + const schemaNames = new Set(); + for (const member of members) { + if (!member.declaration) continue; + const fn = compiler.fullname(member.declaration); + if (fn.hasValue(UNHANDLED)) continue; + const fullname = fn.getValue(); + if (!fullname || fullname.length === 0) continue; + if (fullname.length > 1) { + // Qualified name like auth.users -> schema 'auth' + schemaNames.add(fullname[0]); + } else { + // Unqualified name -> default 'public' schema + schemaNames.add(DEFAULT_SCHEMA_NAME); + } + } + + const schemaSymbols = [...schemaNames].map((name) => new SchemaSymbol(name)); + + return new Report([...members, ...schemaSymbols], errors, warnings); + }, + + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { return Report.create(PASS_THROUGH); }, + bind: bindProgram, + + interpret: interpretProgram, +}; diff --git a/packages/dbml-parse/src/core/global_modules/program/interpret.ts b/packages/dbml-parse/src/core/global_modules/program/interpret.ts new file mode 100644 index 000000000..96b004273 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/program/interpret.ts @@ -0,0 +1,165 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, UNHANDLED, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { isProgramNode } from '@/core/utils/expression'; +import { getTokenPosition } from '../utils'; +import { Alias, Database, Table, Ref, RefEndpoint, Enum, TableGroup, TablePartial, Note, TableRecord, Project, schemaFrom } from '@/core/types/schemaJson'; +import type { RelationCardinality } from '@/core/types/schemaJson'; +import { CompileErrorCode, type CompileError, type CompileWarning, CompileError as CE } from '@/core/types/errors'; +import { validateForeignKeys } from '../records/utils/constraints'; + +function resolveRelationCardinalities (op: string): [RelationCardinality, RelationCardinality] | undefined { + switch (op) { + case '>': return ['*', '1']; + case '<': return ['1', '*']; + case '-': return ['1', '1']; + case '<>': return ['*', '*']; + default: return undefined; + } +} + +export function interpretProgram (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isProgramNode(node)) return Report.create(PASS_THROUGH); + + const token = getTokenPosition(node); + const errors: CompileError[] = []; + const warnings: CompileWarning[] = []; + const db = new Database(token); + + // Interpret each top-level element and collect into the Database + const symbolResult = compiler.nodeSymbol(node); + if (symbolResult.hasValue(UNHANDLED)) return new Report(db); + + const membersResult = compiler.symbolMembers(symbolResult.getValue()); + if (membersResult.hasValue(UNHANDLED)) return new Report(db); + + for (const member of membersResult.getValue()) { + if (!member.declaration) continue; + const result = compiler.interpret(member.declaration); + if (result.hasValue(UNHANDLED)) continue; + errors.push(...result.getErrors()); + warnings.push(...result.getWarnings()); + + const value = result.getValue(); + if (!value) continue; + if (value instanceof Table) db.tables.push(value); + else if (value instanceof Ref) db.refs.push(value); + else if (value instanceof Enum) db.enums.push(value); + else if (value instanceof TableGroup) db.tableGroups.push(value); + else if (value instanceof TablePartial) db.tablePartials.push(value); + else if (value instanceof Note) db.notes.push(value); + else if (value instanceof Project) db.project = value; + else if (value instanceof TableRecord) db.records.push(value); + else if (Array.isArray(value)) { + // interpretTable may return [Table, ...TableRecord] when there are nested records + for (const v of value) { + if (v instanceof Table) db.tables.push(v); + else if (v instanceof TableRecord) db.records.push(v); + } + } + } + + // Extract table aliases + for (const table of db.tables) { + if (table.alias) { + db.aliases.push(schemaFrom(Alias, { + name: table.alias, + kind: 'table' as const, + value: { tableName: table.name, schemaName: table.schemaName }, + token: table.token, + })); + } + } + + // Convert inline refs from table fields into top-level Ref objects + // Inline refs are placed before standalone refs in the output + const inlineRefs: Ref[] = []; + for (const table of db.tables) { + for (const field of table.fields) { + for (const inlineRef of field.inline_refs) { + const cardinalities = resolveRelationCardinalities(inlineRef.relation); + if (!cardinalities) continue; + + const leftEndpoint = schemaFrom(RefEndpoint, { + schemaName: table.schemaName, + tableName: table.name, + fieldNames: [field.name], + relation: cardinalities[0], + token: field.token, + _inlineSource: true, + }); + + const rightEndpoint = schemaFrom(RefEndpoint, { + schemaName: inlineRef.schemaName, + tableName: inlineRef.tableName, + fieldNames: inlineRef.fieldNames, + relation: cardinalities[1], + token: inlineRef.token, + _inlineTarget: true, + }); + + inlineRefs.push(schemaFrom(Ref, { + schemaName: null, + name: null, + endpoints: [rightEndpoint, leftEndpoint], + token: inlineRef.token, + _fromInline: true, + })); + } + } + } + db.refs = [...inlineRefs, ...db.refs]; + + // Validate duplicate records blocks for the same table + { + const recordsByTable = new Map(); + for (const record of db.records) { + const key = `${record.schemaName ?? 'public'}.${record.tableName}`; + if (!recordsByTable.has(key)) { + recordsByTable.set(key, []); + } + recordsByTable.get(key)!.push(record); + } + for (const [, records] of recordsByTable) { + if (records.length > 1) { + const tableName = records[0].tableName; + const msg = `Duplicate Records blocks for the same Table '${tableName}' - A Table can only have one Records block`; + // First block gets (N-1) errors, each subsequent block gets 1 error + // Total: 2*(N-1) errors + for (let i = 1; i < records.length; i++) { + errors.push(new CE( + CompileErrorCode.DUPLICATE_RECORDS_FOR_TABLE, + msg, + records[0] as any, + )); + } + for (let i = 1; i < records.length; i++) { + errors.push(new CE( + CompileErrorCode.DUPLICATE_RECORDS_FOR_TABLE, + msg, + records[i] as any, + )); + } + } + } + } + + // Run FK validation for all records now that all tables/refs/records are collected + const recordTableMap = new Map(); + for (const record of db.records) { + const table = db.tables.find((t) => t.name === record.tableName && (t.schemaName ?? 'public') === (record.schemaName ?? 'public')); + if (table) { + const key = `${record.schemaName ?? 'public'}.${record.tableName}`; + recordTableMap.set(key, { record, table }); + } + } + for (const record of db.records) { + const table = db.tables.find((t) => t.name === record.tableName && (t.schemaName ?? 'public') === (record.schemaName ?? 'public')); + if (table) { + warnings.push(...validateForeignKeys(record, table, db.refs, recordTableMap)); + } + } + + return new Report(db, errors, warnings); +} diff --git a/packages/dbml-parse/src/core/global_modules/program/utils.ts b/packages/dbml-parse/src/core/global_modules/program/utils.ts new file mode 100644 index 000000000..70ca3651a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/program/utils.ts @@ -0,0 +1 @@ +// Module-specific utilities diff --git a/packages/dbml-parse/src/core/global_modules/project/bind.ts b/packages/dbml-parse/src/core/global_modules/project/bind.ts new file mode 100644 index 000000000..e95ce5094 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/project/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindProject (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Project) && !isElementFieldNode(node, ElementKind.Project)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/project/index.ts b/packages/dbml-parse/src/core/global_modules/project/index.ts new file mode 100644 index 000000000..70fcb9b55 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/project/index.ts @@ -0,0 +1,55 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import type { SchemaElement } from '@/core/types/schemaJson'; +import { getNodeMemberSymbols, getSymbolDirectMembers } from '../utils'; +import { interpretProject } from './interpret'; +import { bindProject } from './bind'; + +export const projectModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Project)) { + return new Report(new NodeSymbol({ + kind: SymbolKind.Project, + declaration: node, + })); + } + if (isElementFieldNode(node, ElementKind.Project)) { + return new Report(new NodeSymbol({ kind: SymbolKind.ProjectField, declaration: node })); + } + return Report.create(PASS_THROUGH); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (symbol.isKind(SymbolKind.Project)) { + return getSymbolDirectMembers(compiler, symbol); + } + if (symbol.isKind(SymbolKind.ProjectField)) { + return new Report([]); + } + return Report.create(PASS_THROUGH); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Project)) { + return getNodeMemberSymbols(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { return Report.create(PASS_THROUGH); }, + + bind: bindProject, + + interpret (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Project)) { + return interpretProject(compiler, node); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/global_modules/project/interpret.ts b/packages/dbml-parse/src/core/global_modules/project/interpret.ts new file mode 100644 index 000000000..56479a13a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/project/interpret.ts @@ -0,0 +1,96 @@ +import { isElementNode, getBody } from '@/core/utils/expression'; +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { FunctionApplicationNode, ElementDeclarationNode, BlockExpressionNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { extractQuotedStringToken, extractVariableFromExpression } from '@/core/syntax/utils'; +import { getTokenPosition, normalizeNoteContent } from '../utils'; +import { Project, schemaFrom } from '@/core/types/schemaJson'; +import type { TokenPosition } from '@/core/types/schemaJson'; +import type { CompileError } from '@/core/types/errors'; + +export function interpretProject (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Project)) return Report.create(PASS_THROUGH); + + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const projectName = name?.at(-1) ?? null; + + const token = getTokenPosition(node); + const errors: CompileError[] = []; + + // Extract properties from project body fields (e.g. `database_type: 'PostgreSQL'`) + const extraProps: Record = {}; + let note: { value: string; token: TokenPosition } | undefined; + + const body = node.body; + if (body instanceof BlockExpressionNode) { + for (const field of body.body) { + // Handle Note sub-element: `Note: 'some note'` or `Note { 'some note' }` + if (field instanceof ElementDeclarationNode && isElementNode(field, ElementKind.Note)) { + let noteContent: string | undefined; + if (field.bodyColon && field.body instanceof FunctionApplicationNode) { + noteContent = extractQuotedStringToken(field.body.callee); + } else if (!field.bodyColon && field.body instanceof BlockExpressionNode) { + const firstField = field.body.body[0]; + if (firstField instanceof FunctionApplicationNode) { + noteContent = extractQuotedStringToken(firstField.callee); + } + } + if (noteContent !== undefined) { + note = { + value: normalizeNoteContent(noteContent), + token: getTokenPosition(field), + }; + } + continue; + } + + // Handle key-value fields: `database_type: 'PostgreSQL'` + // These are parsed as ElementDeclarationNode with type=key, bodyColon=:, body=FunctionApplicationNode + if (field instanceof ElementDeclarationNode && field.bodyColon && field.body instanceof FunctionApplicationNode) { + const fieldName = field.type?.value; + if (fieldName) { + const fieldValue = extractQuotedStringToken(field.body.callee); + if (fieldValue !== undefined) { + extraProps[fieldName] = fieldValue; + } + } + } else if (field instanceof FunctionApplicationNode) { + const fieldName = extractVariableFromExpression(field.callee); + if (fieldName && field.args.length > 0) { + const fieldValue = extractQuotedStringToken(field.args[0]); + if (fieldValue !== undefined) { + extraProps[fieldName] = fieldValue; + } + } + } + } + } + + // Also check settings for note + const settingsResult = compiler.settings(node); + errors.push(...settingsResult.getErrors()); + + if (!note && !settingsResult.hasValue(UNHANDLED)) { + const settings = settingsResult.getValue(); + if (settings.get(SettingName.Note)?.[0]?.value) { + const noteValue = extractQuotedStringToken(settings.get(SettingName.Note)![0].value); + if (noteValue !== undefined) { + note = { + value: normalizeNoteContent(noteValue), + token: getTokenPosition(settings.get(SettingName.Note)![0]), + }; + } + } + } + + return new Report(schemaFrom(Project, { + name: projectName, + token, + ...(note && { note }), + ...extraProps, + }), errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/project/utils.ts b/packages/dbml-parse/src/core/global_modules/project/utils.ts new file mode 100644 index 000000000..70ca3651a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/project/utils.ts @@ -0,0 +1 @@ +// Module-specific utilities diff --git a/packages/dbml-parse/src/core/global_modules/records/bind.ts b/packages/dbml-parse/src/core/global_modules/records/bind.ts new file mode 100644 index 000000000..9f52fd5d8 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/records/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindRecords (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Records)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/records/index.ts b/packages/dbml-parse/src/core/global_modules/records/index.ts new file mode 100644 index 000000000..b0c179d98 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/records/index.ts @@ -0,0 +1,90 @@ +import { isElementNode, isExpressionAVariableNode, isInsideElementBody } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import { CallExpressionNode, ElementDeclarationNode, TupleExpressionNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import type { GlobalModule } from '../types'; +import { PASS_THROUGH, UNHANDLED, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { getSymbolDirectMembers } from '../utils'; +import { nodeRefereeOfRecordsName, nodeRefereeOfRecordsColumn, nodeRefereeOfEnumValue } from './utils'; +import { interpretRecords } from './interpret'; +import { bindRecords } from './bind'; + +export const recordsModule: GlobalModule = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Records)) { + return Report.create(PASS_THROUGH); + } + + return new Report(new NodeSymbol({ + kind: SymbolKind.Records, + declaration: node, + })); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (!symbol.isKind(SymbolKind.Records)) { + return Report.create(PASS_THROUGH); + } + + return getSymbolDirectMembers(compiler, symbol); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Records)) { + return Report.create(PASS_THROUGH); + } + return new Report([]); + }, + + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isExpressionAVariableNode(node)) return Report.create(PASS_THROUGH); + + const recordsNode = node.parentOfKind(ElementDeclarationNode); + if (!recordsNode?.isKind(ElementKind.Records)) return Report.create(PASS_THROUGH); + + const programNode = compiler.parseFile().getValue().ast; + const globalSymbol = compiler.nodeSymbol(programNode).getValue(); + if (globalSymbol === UNHANDLED) return Report.create(undefined); + + // Column in tuple directly under records: (col1, col2) + const tupleParent = node.parentOfKind(TupleExpressionNode); + if (tupleParent?.parent === recordsNode) { + const tableNode = recordsNode.parentElement; + if (tableNode instanceof ElementDeclarationNode && tableNode.isKind(ElementKind.Table)) { + const tableSymbol = compiler.nodeSymbol(tableNode); + if (!tableSymbol.hasValue(UNHANDLED)) { + return nodeRefereeOfRecordsColumn(compiler, tableSymbol.getValue(), node); + } + } + return new Report(undefined); + } + + // Column in call expression args: [schema*].table(col1, col2) + const callParent = node.parentOfKind(CallExpressionNode); + if (callParent?.parent === recordsNode && tupleParent?.parent === callParent) { + if (callParent.callee) { + const tableReferee = compiler.nodeReferee(callParent.callee); + if (!tableReferee.hasValue(UNHANDLED) && tableReferee.getValue()) { + return nodeRefereeOfRecordsColumn(compiler, tableReferee.getValue()!, node); + } + } + return new Report(undefined); + } + + // Table/schema in call expression callee: [schema*].table(...) + if (callParent?.parent === recordsNode && callParent.callee?.containsEq(node)) { + return nodeRefereeOfRecordsName(compiler, globalSymbol, node); + } + + // Body: data row values - enum.field or schema.enum.field + return nodeRefereeOfEnumValue(compiler, globalSymbol, node); + }, + + bind: bindRecords, + + interpret: interpretRecords, +}; diff --git a/packages/dbml-parse/src/core/global_modules/records/interpret.ts b/packages/dbml-parse/src/core/global_modules/records/interpret.ts new file mode 100644 index 000000000..3d09ee093 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/records/interpret.ts @@ -0,0 +1,686 @@ +import { ElementKind } from '@/core/types/keywords'; +import { + BlockExpressionNode, + CallExpressionNode, + CommaExpressionNode, + ElementDeclarationNode, + FunctionApplicationNode, + ProgramNode, + TupleExpressionNode, +} from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { SymbolKind } from '@/core/types/symbols'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { Table, TableRecord, schemaFrom } from '@/core/types/schemaJson'; +import type { SchemaElement, RecordValue } from '@/core/types/schemaJson'; +import { Column, ColumnType } from '@/core/types/schemaJson'; +import { extractVariableFromExpression, destructureCallExpression, extractQuotedStringToken } from '@/core/syntax/utils'; +import { isExpressionAQuotedString, isExpressionAVariableNode, isExpressionAnIdentifierNode, isExpressionASignedNumberExpression, getNumberTextFromExpression, isElementNode } from '@/core/utils/expression'; +import { getTokenPosition } from '../utils'; +import { CompileErrorCode, type CompileError, type CompileWarning, CompileWarning as CW } from '@/core/types/errors'; +import { validatePrimaryKey, validateUnique, validateForeignKeys } from './utils/constraints'; +import { Ref, Database } from '@/core/types/schemaJson'; +import { + isNullish, isEmptyStringLiteral, + tryExtractBoolean, tryExtractNumeric, tryExtractInteger, tryExtractString, tryExtractDateTime, tryExtractEnum, + extractSignedNumber, +} from './utils/data/values'; +import { + isIntegerType, isFloatType, isNumericType, isBooleanType, isStringType, isDateTimeType, isSerialType, + getRecordValueType, normalizeTypeName, +} from './utils/data/sqlTypes'; +import { isAutoIncrementColumn } from './utils/constraints/helper'; + +// Extract the base type name from a type_name like 'varchar(255)' -> 'varchar' +function baseTypeName (typeName: string): string { + const parenIdx = typeName.indexOf('('); + return parenIdx >= 0 ? typeName.substring(0, parenIdx).trim() : typeName; +} + +// Parse numeric precision and scale from column type args like '10,2' +function parseNumericParams (column: Column): { precision: number; scale: number } | undefined { + const args = column.type.args; + if (!args) return undefined; + const parts = args.split(',').map((s) => s.trim()); + if (parts.length === 2) { + const precision = parseInt(parts[0], 10); + const scale = parseInt(parts[1], 10); + if (!isNaN(precision) && !isNaN(scale)) return { precision, scale }; + } + if (parts.length === 1) { + const precision = parseInt(parts[0], 10); + if (!isNaN(precision)) return { precision, scale: 0 }; + } + return undefined; +} + +// Parse length param from column type args like '255' +function parseLengthParam (column: Column): { length: number } | undefined { + const args = column.type.args; + if (!args) return undefined; + const length = parseInt(args.trim(), 10); + if (!isNaN(length)) return { length }; + return undefined; +} + +export function interpretRecords (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Records)) return Report.create(PASS_THROUGH); + + const token = getTokenPosition(node); + const errors: CompileError[] = []; + + // Resolve which table this records block belongs to + const { table, columns } = resolveTableAndColumns(compiler, node); + + if (!table || columns.length === 0) { + return new Report(schemaFrom(TableRecord, { + schemaName: undefined, + tableName: '', + columns: [], + values: [], + token, + }), errors); + } + + // Extract row data from the block body + const values: RecordValue[][] = []; + const rowWarnings: CompileWarning[] = []; + const body = node.body; + if (body instanceof BlockExpressionNode) { + for (const row of body.body) { + if (!(row instanceof FunctionApplicationNode)) continue; + const rowValues = extractRow(compiler, row, columns, node); + errors.push(...rowValues.errors); + rowWarnings.push(...rowValues.warnings); + if (rowValues.values) { + values.push(rowValues.values); + } + } + } + + const tableRecord = schemaFrom(TableRecord, { + schemaName: table.schemaName ?? undefined, + tableName: table.name, + columns: columns.map((c) => c.name), + values, + token, + }); + + // Validate PK, UNIQUE constraints (FK validation deferred to avoid cycles) + const warnings: CompileWarning[] = [ + ...rowWarnings, + ...validatePrimaryKey(tableRecord, table), + ...validateUnique(tableRecord, table), + ]; + + return new Report(tableRecord, errors, warnings); +} + +// Build a minimal Table object from an ElementDeclarationNode without calling interpret (avoids cycles) +function buildTableFromElement (compiler: Compiler, tableNode: ElementDeclarationNode): Table | undefined { + const fnResult = compiler.fullname(tableNode); + const name = !fnResult.hasValue(UNHANDLED) ? fnResult.getValue() : undefined; + const tableName = name?.at(-1) ?? ''; + const schemaName = name && name.length > 1 ? name.slice(0, -1).join('.') : null; + + const symResult = compiler.nodeSymbol(tableNode); + if (symResult.hasValue(UNHANDLED)) return undefined; + const membersResult = compiler.symbolMembers(symResult.getValue()); + const members = !membersResult.hasValue(UNHANDLED) ? membersResult.getValue() : []; + + const fields: Column[] = []; + for (const member of members) { + if (!member.declaration) continue; + // Skip Records members and any ElementDeclarationNode members (like nested records) + // to avoid cycles. Only FunctionApplicationNode members (table columns) are relevant. + if (member.isKind(SymbolKind.Records)) continue; + if (member.declaration instanceof ElementDeclarationNode) continue; + const fieldResult = compiler.interpret(member.declaration); + if (!fieldResult.hasValue(UNHANDLED)) { + const field = fieldResult.getValue(); + if (field && 'type' in field && 'name' in field && !('endpoints' in field)) { + fields.push(field as unknown as Column); + } + } + } + + // Extract settings for pk/unique info + const settingsResult = compiler.settings(tableNode); + const token = getTokenPosition(tableNode); + + return schemaFrom(Table, { + name: tableName, + schemaName, + alias: null, + fields, + checks: [], + partials: [], + indexes: [], + token, + }); +} + +function collectAllRefs (compiler: Compiler): Ref[] { + const ast = compiler.parseFile().getValue().ast; + const programSymbol = compiler.nodeSymbol(ast); + if (programSymbol.hasValue(UNHANDLED)) return []; + + const members = compiler.symbolMembers(programSymbol.getValue()); + if (members.hasValue(UNHANDLED)) return []; + + const refs: Ref[] = []; + for (const member of members.getValue()) { + if (!member.isKind(SymbolKind.Ref) || !member.declaration) continue; + const result = compiler.interpret(member.declaration); + if (!result.hasValue(UNHANDLED)) { + const value = result.getValue(); + if (value instanceof Ref) refs.push(value); + } + } + return refs; +} + +function collectAllRecords (compiler: Compiler): Map { + const map = new Map(); + const ast = compiler.parseFile().getValue().ast; + const programSymbol = compiler.nodeSymbol(ast); + if (programSymbol.hasValue(UNHANDLED)) return map; + + const members = compiler.symbolMembers(programSymbol.getValue()); + if (members.hasValue(UNHANDLED)) return map; + + for (const member of members.getValue()) { + if (!member.isKind(SymbolKind.Table) || !member.declaration) continue; + const tableResult = compiler.interpret(member.declaration); + if (tableResult.hasValue(UNHANDLED)) continue; + const table = tableResult.getValue(); + if (!(table instanceof Table)) continue; + + // Find records for this table + for (const m of members.getValue()) { + if (!m.isKind(SymbolKind.Records) || !m.declaration) continue; + const recResult = compiler.interpret(m.declaration); + if (recResult.hasValue(UNHANDLED)) continue; + const rec = recResult.getValue(); + if (rec && 'tableName' in rec && (rec as TableRecord).tableName === table.name) { + const key = `${table.schemaName ?? 'public'}.${table.name}`; + map.set(key, { record: rec as TableRecord, table }); + } + } + } + + return map; +} + +// Resolve the owning table and the column order for this records block +function resolveTableAndColumns ( + compiler: Compiler, + node: ElementDeclarationNode, +): { table: Table | undefined; columns: Column[] } { + const parent = node.parentElement; + + // Nested records: parent is a table element + if (parent instanceof ElementDeclarationNode && parent.isKind(ElementKind.Table)) { + // Build a minimal Table from the parent without calling interpret (avoids cycles) + const table = buildTableFromElement(compiler, parent); + if (!table) return { table: undefined, columns: [] }; + + // If name is a tuple like `records (col1, col2)`, use that as column order + if (node.name instanceof TupleExpressionNode) { + const columns = node.name.elementList.map((e) => { + const colName = extractVariableFromExpression(e); + return table.fields.find((f) => f.name === colName); + }).filter((c): c is Column => c !== undefined); + return { table, columns }; + } + + return { table, columns: table.fields }; + } + + // Top-level records: name is a call expression like `table_name(col1, col2)` + const callResult = destructureCallExpression(node.name); + if (!callResult) return { table: undefined, columns: [] }; + + // Resolve the table by looking up the symbol + const tableNameFragments = callResult.variables.map((v) => v.expression.variable?.value ?? ''); + const tableName = tableNameFragments.at(-1) ?? ''; + + // Find the table among program-level symbols + const ast = compiler.parseFile().getValue().ast; + const programSymbol = compiler.nodeSymbol(ast); + if (programSymbol.hasValue(UNHANDLED)) return { table: undefined, columns: [] }; + const topMembers = compiler.symbolMembers(programSymbol.getValue()); + if (topMembers.hasValue(UNHANDLED)) return { table: undefined, columns: [] }; + + const tableSymbol = topMembers.getValue().find((m: import('@/core/types/symbols').NodeSymbol) => { + if (!m.isKind(SymbolKind.Table) || !m.declaration) return false; + const fn = compiler.fullname(m.declaration); + if (fn.hasValue(UNHANDLED)) return false; + return fn.getValue()?.at(-1) === tableName; + }); + + if (!tableSymbol?.declaration) return { table: undefined, columns: [] }; + + // Try to use the fully interpreted table (includes indexes etc.) + // Fall back to minimal buildTableFromElement if interpret not available + let table: Table | undefined; + const interpretResult = compiler.interpret(tableSymbol.declaration); + if (!interpretResult.hasValue(UNHANDLED)) { + const val = interpretResult.getValue(); + if (val instanceof Table) { + table = val; + } else if (Array.isArray(val)) { + table = val.find((v): v is Table => v instanceof Table); + } + } + if (!table) { + table = buildTableFromElement(compiler, tableSymbol.declaration as ElementDeclarationNode); + } + if (!table) return { table: undefined, columns: [] }; + + // Use explicit column list from call args if provided + if (callResult.args.length > 0) { + const columns = callResult.args.map((a) => { + const colName = a.expression.variable?.value; + return table.fields.find((f) => f.name === colName); + }).filter((c): c is Column => c !== undefined); + return { table, columns }; + } + + return { table, columns: table.fields }; +} + +// Extract values from a single row +function extractRow ( + compiler: Compiler, + row: FunctionApplicationNode, + columns: Column[], + recordsNode: ElementDeclarationNode, +): { values: RecordValue[] | null; errors: CompileError[]; warnings: CompileWarning[] } { + const args = row.callee instanceof CommaExpressionNode + ? row.callee.elementList + : row.callee ? [row.callee] : []; + + if (args.length !== columns.length) { + // Row length mismatch, skip + return { values: null, errors: [], warnings: [] }; + } + + const values: RecordValue[] = []; + const warnings: CompileWarning[] = []; + for (let i = 0; i < columns.length; i++) { + const { value, warnings: cellWarnings } = extractValue(compiler, args[i], columns[i], recordsNode); + values.push(value); + warnings.push(...cellWarnings); + } + + return { values, errors: [], warnings }; +} + +// Check if a column is an enum type +// If isEnum was explicitly set, use that. Otherwise, check if type is not a known SQL type. +function isEnumColumn (column: Column): boolean { + if (column.type.isEnum === true) return true; + const baseName = baseTypeName(column.type.type_name); + // If type is not recognized as any known SQL type category, it's likely an enum + return baseName !== '' && + !isIntegerType(baseName) && + !isFloatType(baseName) && + !isBooleanType(baseName) && + !isStringType(baseName) && + !isDateTimeType(baseName) && + !isSerialType(baseName); +} + +// Get all valid enum values for a column +function getEnumValues (compiler: Compiler, column: Column): Set | undefined { + if (!isEnumColumn(column)) return undefined; + + const ast = compiler.parseFile().getValue().ast; + const programSymbol = compiler.nodeSymbol(ast); + if (programSymbol.hasValue(UNHANDLED)) return undefined; + + const members = compiler.symbolMembers(programSymbol.getValue()); + if (members.hasValue(UNHANDLED)) return undefined; + + for (const member of members.getValue()) { + if (!member.isKind(SymbolKind.Enum) || !member.declaration) continue; + const fn = compiler.fullname(member.declaration); + if (fn.hasValue(UNHANDLED)) continue; + const fullname = fn.getValue(); + if (!fullname) continue; + + const enumTypeName = column.type.type_name; + const enumSchemaName = column.type.schemaName; + + // Match by name: either just name or schema.name + const enumName = fullname.at(-1); + const enumSchema = fullname.length > 1 ? fullname.slice(0, -1).join('.') : null; + + if (enumName === enumTypeName && enumSchema === enumSchemaName) { + // Get enum members + const enumSymbol = compiler.nodeSymbol(member.declaration); + if (enumSymbol.hasValue(UNHANDLED)) continue; + const enumMembers = compiler.symbolMembers(enumSymbol.getValue()); + if (enumMembers.hasValue(UNHANDLED)) continue; + + const values = new Set(); + for (const em of enumMembers.getValue()) { + if (em.declaration) { + const emFn = compiler.fullname(em.declaration); + if (!emFn.hasValue(UNHANDLED)) { + const emName = emFn.getValue()?.at(-1); + if (emName) values.add(emName); + } + } + } + return values; + } + } + + return undefined; +} + +// Extract a single cell value as a RecordValue, using column type for type-aware conversion +function extractValue ( + compiler: Compiler, + node: SyntaxNode, + column: Column, + recordsNode: ElementDeclarationNode, +): { value: RecordValue; warnings: CompileWarning[] } { + const warnings: CompileWarning[] = []; + const colTypeName = baseTypeName(column.type.type_name); + const isEnum = isEnumColumn(column); + const recordValueType = getRecordValueType(colTypeName, isEnum); + + // Handle NULL (null keyword or EmptyNode) + if (isNullish(node)) { + // Check NOT NULL constraint + const nullWarnings = validateNullConstraint(column, recordsNode); + return { + value: { value: null, type: recordValueType }, + warnings: nullWarnings, + }; + } + + // Handle empty string for non-string types -> null + if (isEmptyStringLiteral(node) && !isStringType(colTypeName)) { + return { + value: { value: null, type: recordValueType }, + warnings: [], + }; + } + + // Boolean column + if (isBooleanType(colTypeName)) { + const boolVal = tryExtractBoolean(node); + if (boolVal !== null) { + return { value: { value: boolVal, type: 'bool' }, warnings: [] }; + } + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid boolean value for column '${column.name}'`, + recordsNode as any, + )); + // Still store the raw value + return { value: { value: extractRawValue(node), type: 'bool' }, warnings }; + } + + // Integer column + if (isIntegerType(colTypeName) || isSerialType(colTypeName)) { + // Try numeric extraction + const numVal = extractSignedNumber(node); + if (numVal !== null) { + if (!Number.isInteger(numVal)) { + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid integer value ${numVal} for column '${column.name}': expected integer, got decimal`, + recordsNode as any, + )); + return { value: { value: numVal, type: 'integer' }, warnings }; + } + return { value: { value: numVal, type: 'integer' }, warnings: [] }; + } + // Try string that looks like a number + const strVal = extractQuotedStringToken(node); + if (strVal !== undefined) { + const parsed = Number(strVal); + if (!isNaN(parsed) && Number.isInteger(parsed)) { + return { value: { value: parsed, type: 'integer' }, warnings: [] }; + } + } + // Invalid + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid numeric value for column '${column.name}'`, + recordsNode as any, + )); + return { value: { value: extractRawValue(node), type: 'integer' }, warnings }; + } + + // Float/decimal column + if (isFloatType(colTypeName)) { + const numVal = extractSignedNumber(node); + if (numVal !== null) { + // Validate precision/scale if specified + validateNumericPrecision(numVal, column, recordsNode, warnings); + return { value: { value: numVal, type: 'real' }, warnings }; + } + // Try string that's numeric + const strVal = extractQuotedStringToken(node); + if (strVal !== undefined) { + const parsed = Number(strVal); + if (!isNaN(parsed)) { + validateNumericPrecision(parsed, column, recordsNode, warnings); + return { value: { value: parsed, type: 'real' }, warnings }; + } + } + // Check for boolean/identifier - invalid for numeric + if (isExpressionAVariableNode(node)) { + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid numeric value for column '${column.name}'`, + recordsNode as any, + )); + return { value: { value: extractRawValue(node), type: 'real' }, warnings }; + } + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid numeric value for column '${column.name}'`, + recordsNode as any, + )); + return { value: { value: extractRawValue(node), type: 'real' }, warnings }; + } + + // Datetime column + if (isDateTimeType(colTypeName)) { + const strVal = extractQuotedStringToken(node); + if (strVal !== undefined) { + const dtVal = tryExtractDateTime(node); + if (dtVal !== null) { + return { value: { value: dtVal, type: recordValueType }, warnings: [] }; + } + // String but not a valid datetime - still store as-is + return { value: { value: strVal, type: recordValueType }, warnings: [] }; + } + // Non-string values (numbers, booleans) are invalid for datetime + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid datetime value for column '${column.name}'`, + recordsNode as any, + )); + return { value: { value: extractRawValue(node), type: recordValueType }, warnings }; + } + + // String column + if (isStringType(colTypeName)) { + const strVal = tryExtractString(node); + const finalVal = strVal ?? extractRawValue(node)?.toString() ?? null; + // Validate length + if (typeof finalVal === 'string') { + validateStringLength(finalVal, column, recordsNode, warnings); + } + return { value: { value: finalVal, type: 'string' }, warnings }; + } + + // Enum column + if (isEnum) { + // Enum access (enum.value) is handled by binding, which resolves it + // But we need to validate string literals against enum values + const strVal = extractQuotedStringToken(node); + if (strVal !== undefined) { + const enumValues = getEnumValues(compiler, column); + if (enumValues && !enumValues.has(strVal)) { + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid enum value for column '${column.name}'`, + recordsNode as any, + )); + } + return { value: { value: strVal, type: 'string' }, warnings }; + } + // Numeric literal - invalid for enum + if (isExpressionASignedNumberExpression(node)) { + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid enum value for column '${column.name}'`, + recordsNode as any, + )); + return { value: { value: extractRawValue(node), type: 'string' }, warnings }; + } + // Identifier - could be enum.value access (handled by binding) or boolean + if (isExpressionAVariableNode(node)) { + const val = node.expression.variable.value.toLowerCase(); + if (val === 'true' || val === 'false') { + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Invalid enum value for column '${column.name}'`, + recordsNode as any, + )); + return { value: { value: val === 'true', type: 'string' }, warnings }; + } + // Probably an enum.value access - binding handles this + return { value: { value: node.expression.variable.value, type: 'string' }, warnings }; + } + // Dot access for enum (handled elsewhere in binding) + return { value: { value: extractRawValue(node), type: 'string' }, warnings }; + } + + // Fallback: unknown column type - use basic type detection + return { value: extractValueBasic(node), warnings: [] }; +} + +// Validate NOT NULL constraint for a null value +function validateNullConstraint (column: Column, recordsNode: ElementDeclarationNode): CompileWarning[] { + if (!column.not_null) return []; + // Allow NULL if column has default or increment + if (column.dbdefault || column.increment || isSerialType(column.type.type_name)) return []; + return [new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `NULL not allowed for non-nullable column '${column.name}' without default and increment`, + recordsNode as any, + )]; +} + +// Validate numeric precision and scale +function validateNumericPrecision ( + value: number, + column: Column, + recordsNode: ElementDeclarationNode, + warnings: CompileWarning[], +): void { + const params = parseNumericParams(column); + if (!params) return; + const { precision, scale } = params; + + // Check scale (number of decimal places) + const strVal = value.toString(); + const dotIdx = strVal.indexOf('.'); + if (dotIdx >= 0) { + const actualScale = strVal.length - dotIdx - 1; + if (actualScale > scale) { + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Numeric value ${value} for column '${column.name}' exceeds scale: expected at most ${scale} decimal places, got ${actualScale}`, + recordsNode as any, + )); + } + } + + // Check precision (total digits) + const absStr = Math.abs(value).toString().replace('.', ''); + // Remove leading zeros for counting digits + const digits = absStr.replace(/^0+/, '') || '0'; + if (digits.length > precision) { + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `Numeric value ${value} for column '${column.name}' exceeds precision: expected at most ${precision} total digits, got ${digits.length}`, + recordsNode as any, + )); + } +} + +// Validate string length (UTF-8 byte length) +function validateStringLength ( + value: string, + column: Column, + recordsNode: ElementDeclarationNode, + warnings: CompileWarning[], +): void { + const param = parseLengthParam(column); + if (!param) return; + const maxLength = param.length; + const byteLength = new TextEncoder().encode(value).length; + if (byteLength > maxLength) { + warnings.push(new CW( + CompileErrorCode.INVALID_RECORDS_FIELD, + `String value for column '${column.name}' exceeds maximum length: expected at most ${maxLength} bytes (UTF-8), got ${byteLength} bytes`, + recordsNode as any, + )); + } +} + +// Extract raw value from a node without type-awareness (fallback) +function extractRawValue (node: SyntaxNode): any { + const str = extractQuotedStringToken(node); + if (str !== undefined) return str; + + if (isExpressionASignedNumberExpression(node)) { + const numText = getNumberTextFromExpression(node); + return Number(numText); + } + + if (isExpressionAVariableNode(node)) { + const val = node.expression.variable.value.toLowerCase(); + if (val === 'true') return true; + if (val === 'false') return false; + if (val === 'null') return null; + return node.expression.variable.value; + } + + return null; +} + +// Basic type detection without column type (for unknown column types) +function extractValueBasic (node: SyntaxNode): RecordValue { + const str = extractQuotedStringToken(node); + if (str !== undefined) return { value: str, type: 'string' }; + + if (isExpressionASignedNumberExpression(node)) { + const numText = getNumberTextFromExpression(node); + const numVal = Number(numText); + const isInteger = Number.isInteger(numVal) && !numText.includes('.') && !numText.toLowerCase().includes('e'); + return { value: numVal, type: isInteger ? 'integer' : 'real' }; + } + + if (isExpressionAVariableNode(node)) { + const val = node.expression.variable.value.toLowerCase(); + if (val === 'true' || val === 'false') return { value: val === 'true', type: 'bool' }; + if (val === 'null') return { value: null, type: 'string' }; + return { value: node.expression.variable.value, type: 'string' }; + } + + return { value: null, type: 'string' }; +} diff --git a/packages/dbml-parse/src/core/global_modules/records/utils.ts b/packages/dbml-parse/src/core/global_modules/records/utils.ts new file mode 100644 index 000000000..638233fad --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/records/utils.ts @@ -0,0 +1,93 @@ +import type Compiler from '@/compiler'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { InfixExpressionNode } from '@/core/types/nodes'; +import Report from '@/core/types/report'; +import { UNHANDLED } from '@/constants'; +import { lookupMember, nodeRefereeOfLeftExpression } from '../utils'; +import { type NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { isAccessExpression, isExpressionAVariableNode } from '@/core/utils/expression'; +import { extractVarNameFromPrimaryVariable } from '@/core/syntax/utils'; +import { CompileErrorCode, CompileError } from '@/core/types/errors'; + +// Records name callee: [schema*].table +// Standalone: look up as table or schema +// In access: left is schema -> table/schema +export function nodeRefereeOfRecordsName (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + if (!isAccessExpression(node.parent)) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Table, SymbolKind.Schema] }); + } + + const left = nodeRefereeOfLeftExpression(compiler, node); + if (!left) return new Report(undefined); + + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Table, SymbolKind.Schema] }); + } + + return new Report(undefined); +} + +// Records column ref: column name inside (col1, col2) tuple +// Resolves against the parent table's columns +export function nodeRefereeOfRecordsColumn (compiler: Compiler, tableSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + return lookupMember(compiler, tableSymbol, name, { kinds: [SymbolKind.Column] }); +} + +// Records body enum value: enum.field or schema.enum.field +export function nodeRefereeOfEnumValue (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + // Standalone: ignore (could be a literal like null/true/false) + if (!isAccessExpression(node.parent)) { + return new Report(undefined); + } + + // Right side of access: resolve via left sibling + const left = nodeRefereeOfLeftExpression(compiler, node); + if (left) { + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Enum, SymbolKind.Schema] }); + } + if (left.isKind(SymbolKind.Enum)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.EnumField] }); + } + return new Report(undefined); + } + + // Left side of access: look up as Enum or Schema in program scope + const parent = node.parent as InfixExpressionNode; + if (parent.leftExpression === node) { + // If our parent is also the left side of another access, this is a schema + if (isAccessExpression(parent.parent) && (parent.parent as InfixExpressionNode).leftExpression === parent) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Schema] }); + } + // Look up as Enum in program scope (ignoreNotFound first, then verify) + const result = lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Enum], ignoreNotFound: true }); + const sym = result.getValue(); + if (sym && sym.declaration) { + // Verify the enum is not schema-qualified when accessed without schema + const fn = compiler.fullname(sym.declaration); + if (!fn.hasValue(UNHANDLED) && fn.getValue() && fn.getValue()!.length > 1) { + // Schema-qualified enum accessed without schema prefix - report error + return new Report(undefined, [ + new CompileError( + CompileErrorCode.BINDING_ERROR, + `Enum '${name}' does not exist in Schema 'public'`, + node, + ), + ]); + } + return result; + } + // Not found at all - report error + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Enum] }); + } + + return new Report(undefined); +} diff --git a/packages/dbml-parse/src/core/global_modules/records/utils/constraints/fk.ts b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/fk.ts new file mode 100644 index 000000000..7e170f29b --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/fk.ts @@ -0,0 +1,149 @@ +import type { CompileWarning } from '@/core/types/errors'; +import type { Ref, RefEndpoint, Table, TableRecord } from '@/core/types/schemaJson'; +import { + buildColumnIndex, + extractKeyValueWithDefault, + hasNullWithoutDefaultInKey, + formatFullColumnNames, + formatValues, + createConstraintWarnings, +} from './helper'; +import { DEFAULT_SCHEMA_NAME } from '@/constants'; +import { isEmpty, flatMap } from 'lodash-es'; + +type TableInfo = { + record: TableRecord; + table: Table; +}; + +/** + * Validate foreign key constraints on record data. + * Inline refs are already present in Column objects; explicit refs are passed in. + */ +export function validateForeignKeys ( + record: TableRecord, + table: Table, + allRefs: Ref[], + allRecords: Map, +): CompileWarning[] { + const tableKey = makeTableKey(table.schemaName, table.name); + + return flatMap(allRefs, (ref) => validateRef(ref, tableKey, record, table, allRecords)); +} + +function makeTableKey (schema: string | null | undefined, table: string): string { + return schema ? `${schema}.${table}` : `${DEFAULT_SCHEMA_NAME}.${table}`; +} + +function validateRef ( + ref: Ref, + currentTableKey: string, + currentRecord: TableRecord, + currentTable: Table, + allRecords: Map, +): CompileWarning[] { + if (!ref.endpoints) return []; + + const [endpoint1, endpoint2] = ref.endpoints; + const ep1Key = makeTableKey(endpoint1.schemaName, endpoint1.tableName); + const ep2Key = makeTableKey(endpoint2.schemaName, endpoint2.tableName); + const warnings: CompileWarning[] = []; + + // Bidirectional relationships: 1-1 and many-to-many + const isBidirectional = (endpoint1.relation === '1' && endpoint2.relation === '1') || + (endpoint1.relation === '*' && endpoint2.relation === '*'); + + if (isBidirectional) { + if (ep1Key === currentTableKey) { + const target = allRecords.get(ep2Key); + if (target) { + warnings.push(...validateFkSourceToTarget( + currentRecord, currentTable, endpoint1, + target.record, endpoint2, + )); + } + } + if (ep2Key === currentTableKey) { + const target = allRecords.get(ep1Key); + if (target) { + warnings.push(...validateFkSourceToTarget( + currentRecord, currentTable, endpoint2, + target.record, endpoint1, + )); + } + } + return warnings; + } + + // Many-to-one: validate FK from "many" side to "one" side + if (endpoint1.relation === '*' && endpoint2.relation === '1' && ep1Key === currentTableKey) { + const target = allRecords.get(ep2Key); + if (target) { + warnings.push(...validateFkSourceToTarget( + currentRecord, currentTable, endpoint1, + target.record, endpoint2, + )); + } + } + + if (endpoint1.relation === '1' && endpoint2.relation === '*' && ep2Key === currentTableKey) { + const target = allRecords.get(ep1Key); + if (target) { + warnings.push(...validateFkSourceToTarget( + currentRecord, currentTable, endpoint2, + target.record, endpoint1, + )); + } + } + + return warnings; +} + +/** + * Validate that all source FK values exist in the target's values. + */ +function validateFkSourceToTarget ( + sourceRecord: TableRecord, + sourceTable: Table, + sourceEndpoint: RefEndpoint, + targetRecord: TableRecord, + targetEndpoint: RefEndpoint, +): CompileWarning[] { + if (isEmpty(sourceRecord.values)) return []; + + const sourceColumnIndex = buildColumnIndex(sourceRecord); + const targetColumnIndex = buildColumnIndex(targetRecord); + + // Build set of valid target values + const validFkValues = new Set( + targetRecord.values.map((row) => + extractKeyValueWithDefault(row, targetEndpoint.fieldNames, targetColumnIndex), + ), + ); + + const warnings: CompileWarning[] = []; + + for (const row of sourceRecord.values) { + // Skip rows with NULL values (optional relationships) + if (hasNullWithoutDefaultInKey(row, sourceEndpoint.fieldNames, sourceColumnIndex)) continue; + + const fkValue = extractKeyValueWithDefault(row, sourceEndpoint.fieldNames, sourceColumnIndex); + if (!validFkValues.has(fkValue)) { + const sourceColumnRef = formatFullColumnNames( + sourceTable.schemaName, + sourceTable.name, + sourceEndpoint.fieldNames, + ); + const targetColumnRef = formatFullColumnNames( + targetEndpoint.schemaName, + targetEndpoint.tableName, + targetEndpoint.fieldNames, + ); + const valueStr = formatValues(row, sourceEndpoint.fieldNames, sourceColumnIndex); + const message = `FK violation: ${sourceColumnRef} = ${valueStr} does not exist in ${targetColumnRef}`; + warnings.push(...createConstraintWarnings(sourceRecord, message)); + } + } + + return warnings; +} diff --git a/packages/dbml-parse/src/core/global_modules/records/utils/constraints/helper.ts b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/helper.ts new file mode 100644 index 000000000..721fbd146 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/helper.ts @@ -0,0 +1,129 @@ +import type { RecordValue, Column, TableRecord } from '@/core/types/schemaJson'; +import { CompileWarning, CompileErrorCode } from '@/core/types/errors'; +import { isSerialType } from '../data'; + +/** + * Build a column name -> index map from a TableRecord's columns array. + */ +export function buildColumnIndex (record: TableRecord): Map { + const index = new Map(); + for (let i = 0; i < record.columns.length; i++) { + index.set(record.columns[i], i); + } + return index; +} + +/** + * Extract a composite key string from a row using column names and positional access. + * Substitutes column defaults when the cell value is null/undefined. + */ +export function extractKeyValueWithDefault ( + row: RecordValue[], + columnNames: string[], + columnIndex: Map, + columns?: (Column | undefined)[], +): string { + return columnNames.map((name, idx) => { + const colIdx = columnIndex.get(name); + const value = colIdx !== undefined ? row[colIdx]?.value : undefined; + + if ((value === null || value === undefined) && columns && columns[idx]) { + const column = columns[idx]; + if (column?.dbdefault) { + return JSON.stringify(column.dbdefault.value); + } + } + + return JSON.stringify(value); + }).join('|'); +} + +/** + * Check if any column in the key has a null value without a default. + */ +export function hasNullWithoutDefaultInKey ( + row: RecordValue[], + columnNames: string[], + columnIndex: Map, + columns?: (Column | undefined)[], +): boolean { + return columnNames.some((name, idx) => { + const colIdx = columnIndex.get(name); + const value = colIdx !== undefined ? row[colIdx]?.value : undefined; + + if ((value === null || value === undefined) && columns && columns[idx]) { + const column = columns[idx]; + if (column?.dbdefault) { + return false; + } + } + + return value === null || value === undefined; + }); +} + +export function isAutoIncrementColumn (column: Column): boolean { + return (column.increment || false) || isSerialType(column.type.type_name); +} + +export function hasNotNullWithDefault (column: Column): boolean { + return (column.not_null || false) && !!column.dbdefault; +} + +export function formatFullColumnName ( + schemaName: string | null | undefined, + tableName: string, + columnName: string, +): string { + if (schemaName) { + return `${schemaName}.${tableName}.${columnName}`; + } + return `${tableName}.${columnName}`; +} + +export function formatFullColumnNames ( + schemaName: string | null | undefined, + tableName: string, + columnNames: string[], +): string { + if (columnNames.length === 1) { + return formatFullColumnName(schemaName, tableName, columnNames[0]); + } + const formatted = columnNames.map((col) => formatFullColumnName(schemaName, tableName, col)); + return `(${formatted.join(', ')})`; +} + +/** + * Format values for display in error messages. + * e.g. single value: `1`, multiple: `(1, "a")` + */ +export function formatValues ( + row: RecordValue[], + columnNames: string[], + columnIndex: Map, +): string { + if (columnNames.length === 1) { + const colIdx = columnIndex.get(columnNames[0]); + return JSON.stringify(colIdx !== undefined ? row[colIdx]?.value : null); + } + const values = columnNames.map((col) => { + const colIdx = columnIndex.get(col); + return JSON.stringify(colIdx !== undefined ? row[colIdx]?.value : null); + }).join(', '); + return `(${values})`; +} + +/** + * Create constraint warnings for a record. + * Uses the record's token as location since we don't have per-row nodes. + */ +export function createConstraintWarnings ( + record: TableRecord, + message: string, +): CompileWarning[] { + return [new CompileWarning( + CompileErrorCode.INVALID_RECORDS_FIELD, + message, + record as any, + )]; +} diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/index.ts b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/index.ts similarity index 100% rename from packages/dbml-parse/src/core/interpreter/records/utils/constraints/index.ts rename to packages/dbml-parse/src/core/global_modules/records/utils/constraints/index.ts diff --git a/packages/dbml-parse/src/core/global_modules/records/utils/constraints/pk.ts b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/pk.ts new file mode 100644 index 000000000..ae971f701 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/pk.ts @@ -0,0 +1,196 @@ +import { CompileWarning, CompileErrorCode } from '@/core/types/errors'; +import type { Table, Column, TableRecord, RecordValue } from '@/core/types/schemaJson'; +import { + buildColumnIndex, + extractKeyValueWithDefault, + hasNullWithoutDefaultInKey, + isAutoIncrementColumn, + formatFullColumnNames, + formatValues, + createConstraintWarnings, +} from './helper'; +import { isSerialType } from '../data'; +import { compact, isEmpty, difference, flatMap } from 'lodash-es'; + +const getConstraintType = (columnCount: number) => + columnCount > 1 ? 'Composite PK' : 'PK'; + +/** + * Validate primary key constraints on record data. + * The table is expected to have merged partials already. + */ +export function validatePrimaryKey (record: TableRecord, table: Table): CompileWarning[] { + if (isEmpty(record.values)) return []; + + const pkConstraints = collectPkConstraints(table); + const columnIndex = buildColumnIndex(record); + const availableColumns = new Set(record.columns); + const columnMap = new Map(table.fields.map((f) => [f.name, f])); + + return flatMap(pkConstraints, (pkColumns) => + validatePkConstraint(pkColumns, record, availableColumns, columnMap, columnIndex, table), + ); +} + +function validatePkConstraint ( + pkColumns: string[], + record: TableRecord, + availableColumns: Set, + columnMap: Map, + columnIndex: Map, + table: Table, +): CompileWarning[] { + // Check for missing columns + const missingWarnings = checkMissingPkColumns( + pkColumns, + availableColumns, + columnMap, + table, + record, + ); + if (!isEmpty(missingWarnings)) return missingWarnings; + + // Get column definitions + const pkColumnFields = compact(pkColumns.map((col) => columnMap.get(col))); + const areAllColumnsAutoIncrement = pkColumnFields.every((col) => + col && isAutoIncrementColumn(col), + ); + + // Separate rows with NULL and rows without + const rowsWithNull: number[] = []; + const rowsWithoutNull: number[] = []; + for (let i = 0; i < record.values.length; i++) { + if (hasNullWithoutDefaultInKey(record.values[i], pkColumns, columnIndex, pkColumnFields)) { + rowsWithNull.push(i); + } else { + rowsWithoutNull.push(i); + } + } + + // Validate NULL rows (only warn if not all columns are auto-increment) + const nullWarnings = areAllColumnsAutoIncrement + ? [] + : createNullWarnings(rowsWithNull, pkColumns, table, record); + + // Find duplicate rows + const duplicateWarnings = findDuplicateWarnings( + rowsWithoutNull, + record, + pkColumns, + pkColumnFields, + columnIndex, + table, + ); + + return [...nullWarnings, ...duplicateWarnings]; +} + +function createNullWarnings ( + rowIndices: number[], + pkColumns: string[], + table: Table, + record: TableRecord, +): CompileWarning[] { + if (isEmpty(rowIndices)) return []; + + const constraintType = getConstraintType(pkColumns.length); + const columnRef = formatFullColumnNames( + table.schemaName, + table.name, + pkColumns, + ); + const message = `NULL in ${constraintType}: ${columnRef} cannot be NULL`; + + // For composite PKs, report 2 warnings per NULL row (one for the violation, one for the constraint) + // For simple PKs, report 1 warning per NULL row + const warningsPerRow = pkColumns.length > 1 ? 2 : 1; + return flatMap(rowIndices, () => { + const warnings: CompileWarning[] = []; + for (let i = 0; i < warningsPerRow; i++) { + warnings.push(...createConstraintWarnings(record, message)); + } + return warnings; + }); +} + +function findDuplicateWarnings ( + rowIndices: number[], + record: TableRecord, + pkColumns: string[], + pkColumnFields: Column[], + columnIndex: Map, + table: Table, +): CompileWarning[] { + // Group rows by their PK value + const seen = new Map(); + const flagged = new Set(); // Track which keys have already had their first occurrence flagged + const warnings: CompileWarning[] = []; + const isComposite = pkColumns.length > 1; + + for (const idx of rowIndices) { + const row = record.values[idx]; + const key = extractKeyValueWithDefault(row, pkColumns, columnIndex, pkColumnFields); + if (seen.has(key)) { + const constraintType = getConstraintType(pkColumns.length); + const columnRef = formatFullColumnNames( + table.schemaName, + table.name, + pkColumns, + ); + const valueStr = formatValues(row, pkColumns, columnIndex); + const message = `Duplicate ${constraintType}: ${columnRef} = ${valueStr}`; + // For composite PKs, also flag the first occurrence + if (isComposite && !flagged.has(key)) { + flagged.add(key); + warnings.push(...createConstraintWarnings(record, message)); + } + // Flag this duplicate occurrence + warnings.push(...createConstraintWarnings(record, message)); + } else { + seen.set(key, idx); + } + } + + return warnings; +} + +function collectPkConstraints (table: Table): string[][] { + return [ + ...table.fields.filter((field) => field.pk).map((field) => [field.name]), + ...table.indexes.filter((index) => index.pk).map((index) => index.columns.map((c) => c.value)), + ]; +} + +function checkMissingPkColumns ( + pkColumns: string[], + availableColumns: Set, + columnMap: Map, + table: Table, + record: TableRecord, +): CompileWarning[] { + const missingColumns = difference(pkColumns, Array.from(availableColumns)); + if (isEmpty(missingColumns)) return []; + + // Filter to only those without defaults + const hasNoDefaultValue = (colName: string): boolean => { + const col = columnMap.get(colName); + return !!col && !col.increment && !isSerialType(col.type.type_name) && !col.dbdefault; + }; + + const missingWithoutDefaults = missingColumns.filter(hasNoDefaultValue); + if (isEmpty(missingWithoutDefaults)) return []; + + const constraintType = getConstraintType(missingWithoutDefaults.length); + const columnRef = formatFullColumnNames( + table.schemaName, + table.name, + missingWithoutDefaults, + ); + const message = `${constraintType}: Column ${columnRef} is missing from record and has no default value`; + + return record.values.map(() => new CompileWarning( + CompileErrorCode.INVALID_RECORDS_FIELD, + message, + record as any, + )); +} diff --git a/packages/dbml-parse/src/core/global_modules/records/utils/constraints/unique.ts b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/unique.ts new file mode 100644 index 000000000..2cdcb31b1 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/records/utils/constraints/unique.ts @@ -0,0 +1,79 @@ +import type { CompileWarning } from '@/core/types/errors'; +import type { Table, Column, TableRecord } from '@/core/types/schemaJson'; +import { + buildColumnIndex, + extractKeyValueWithDefault, + hasNullWithoutDefaultInKey, + formatFullColumnNames, + formatValues, + createConstraintWarnings, +} from './helper'; +import { compact, isEmpty, flatMap } from 'lodash-es'; + +const getConstraintType = (columnCount: number) => + columnCount > 1 ? 'Composite UNIQUE' : 'UNIQUE'; + +/** + * Validate unique constraints on record data. + * The table is expected to have merged partials already. + */ +export function validateUnique (record: TableRecord, table: Table): CompileWarning[] { + if (isEmpty(record.values)) return []; + + const uniqueConstraints = collectUniqueConstraints(table); + const columnIndex = buildColumnIndex(record); + const columnMap = new Map(table.fields.map((f) => [f.name, f])); + + return flatMap(uniqueConstraints, (uniqueColumns) => { + const uniqueColumnFields = compact(uniqueColumns.map((col) => columnMap.get(col))); + return checkUniqueDuplicates(record, uniqueColumns, uniqueColumnFields, columnIndex, table); + }); +} + +function collectUniqueConstraints (table: Table): string[][] { + return [ + ...table.fields.filter((field) => field.unique).map((field) => [field.name]), + ...table.indexes.filter((index) => index.unique).map((index) => index.columns.map((c) => c.value)), + ]; +} + +function checkUniqueDuplicates ( + record: TableRecord, + uniqueColumns: string[], + uniqueColumnFields: (Column | undefined)[], + columnIndex: Map, + table: Table, +): CompileWarning[] { + const warnings: CompileWarning[] = []; + const seen = new Map(); + const flagged = new Set(); + + for (let i = 0; i < record.values.length; i++) { + const row = record.values[i]; + // NULLs don't conflict in UNIQUE constraints (SQL standard) + if (hasNullWithoutDefaultInKey(row, uniqueColumns, columnIndex, uniqueColumnFields)) continue; + + const key = extractKeyValueWithDefault(row, uniqueColumns, columnIndex, uniqueColumnFields); + if (seen.has(key)) { + const constraintType = getConstraintType(uniqueColumns.length); + const columnRef = formatFullColumnNames( + table.schemaName, + table.name, + uniqueColumns, + ); + const valueStr = formatValues(row, uniqueColumns, columnIndex); + const message = `Duplicate ${constraintType}: ${columnRef} = ${valueStr}`; + // Flag the first occurrence if not already flagged + if (!flagged.has(key)) { + flagged.add(key); + warnings.push(...createConstraintWarnings(record, message)); + } + // Flag this duplicate occurrence + warnings.push(...createConstraintWarnings(record, message)); + } else { + seen.set(key, i); + } + } + + return warnings; +} diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/data/index.ts b/packages/dbml-parse/src/core/global_modules/records/utils/data/index.ts similarity index 100% rename from packages/dbml-parse/src/core/interpreter/records/utils/data/index.ts rename to packages/dbml-parse/src/core/global_modules/records/utils/data/index.ts diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/data/sqlTypes.ts b/packages/dbml-parse/src/core/global_modules/records/utils/data/sqlTypes.ts similarity index 75% rename from packages/dbml-parse/src/core/interpreter/records/utils/data/sqlTypes.ts rename to packages/dbml-parse/src/core/global_modules/records/utils/data/sqlTypes.ts index 0d359108b..03e85e29f 100644 --- a/packages/dbml-parse/src/core/interpreter/records/utils/data/sqlTypes.ts +++ b/packages/dbml-parse/src/core/global_modules/records/utils/data/sqlTypes.ts @@ -1,10 +1,3 @@ -import { - CallExpressionNode, - FunctionApplicationNode, -} from '@/core/parser/nodes'; -import { extractNumericLiteral } from '@/core/analyzer/utils'; -import { ColumnSymbol } from '@/core/analyzer/symbol/symbols'; - export type SqlDialect = 'mysql' | 'postgres' | 'mssql' | 'oracle' | 'snowflake'; // Dialect-specific type mappings @@ -74,7 +67,6 @@ export function isIntegerType (type: string, dialect?: SqlDialect): boolean { if (dialect) { return DIALECT_INTEGER_TYPES[dialect].has(normalized); } - // Check if any dialect has this type return Object.values(DIALECT_INTEGER_TYPES).some((set) => set.has(normalized)); } @@ -83,7 +75,6 @@ export function isFloatType (type: string, dialect?: SqlDialect): boolean { if (dialect) { return DIALECT_FLOAT_TYPES[dialect].has(normalized); } - // Check if any dialect has this type return Object.values(DIALECT_FLOAT_TYPES).some((set) => set.has(normalized)); } @@ -96,7 +87,6 @@ export function isBooleanType (type: string, dialect?: SqlDialect): boolean { if (dialect) { return DIALECT_BOOL_TYPES[dialect].has(normalized); } - // Check if any dialect has this type return Object.values(DIALECT_BOOL_TYPES).some((set) => set.has(normalized)); } @@ -105,7 +95,6 @@ export function isStringType (type: string, dialect?: SqlDialect): boolean { if (dialect) { return DIALECT_STRING_TYPES[dialect].has(normalized); } - // Check if any dialect has this type return Object.values(DIALECT_STRING_TYPES).some((set) => set.has(normalized)); } @@ -114,7 +103,6 @@ export function isBinaryType (type: string, dialect?: SqlDialect): boolean { if (dialect) { return DIALECT_BINARY_TYPES[dialect].has(normalized); } - // Check if any dialect has this type return Object.values(DIALECT_BINARY_TYPES).some((set) => set.has(normalized)); } @@ -123,7 +111,6 @@ export function isDateTimeType (type: string, dialect?: SqlDialect): boolean { if (dialect) { return DIALECT_DATETIME_TYPES[dialect].has(normalized); } - // Check if any dialect has this type return Object.values(DIALECT_DATETIME_TYPES).some((set) => set.has(normalized)); } @@ -132,44 +119,9 @@ export function isSerialType (type: string, dialect?: SqlDialect): boolean { if (dialect) { return DIALECT_SERIAL_TYPES[dialect].has(normalized); } - // Check if any dialect has this type return Object.values(DIALECT_SERIAL_TYPES).some((set) => set.has(normalized)); } -// Get type node from a column symbol's declaration -function getTypeNode (columnSymbol: ColumnSymbol) { - const declaration = columnSymbol.declaration; - if (!(declaration instanceof FunctionApplicationNode)) { - return null; - } - return declaration.args[0] || null; -} - -// Get numeric type parameters (precision, scale) from a column (e.g., decimal(10, 2)) -export function getNumericTypeParams (columnSymbol: ColumnSymbol): { precision?: number; scale?: number } { - const typeNode = getTypeNode(columnSymbol); - if (!(typeNode instanceof CallExpressionNode)) return {}; - if (!typeNode.argumentList || typeNode.argumentList.elementList.length !== 2) return {}; - - const precision = extractNumericLiteral(typeNode.argumentList.elementList[0]); - const scale = extractNumericLiteral(typeNode.argumentList.elementList[1]); - if (precision === null || scale === null) return {}; - - return { precision: Math.trunc(precision), scale: Math.trunc(scale) }; -} - -// Get length type parameter from a column (e.g., varchar(255)) -export function getLengthTypeParam (columnSymbol: ColumnSymbol): { length?: number } { - const typeNode = getTypeNode(columnSymbol); - if (!(typeNode instanceof CallExpressionNode)) return {}; - if (!typeNode.argumentList || typeNode.argumentList.elementList.length !== 1) return {}; - - const length = extractNumericLiteral(typeNode.argumentList.elementList[0]); - if (length === null) return {}; - - return { length: Math.trunc(length) }; -} - // Get the record value type based on SQL type // Returns: 'string' | 'bool' | 'integer' | 'real' | 'date' | 'time' | 'datetime' | original type export function getRecordValueType (sqlType: string, isEnum: boolean): string { diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/data/values.ts b/packages/dbml-parse/src/core/global_modules/records/utils/data/values.ts similarity index 71% rename from packages/dbml-parse/src/core/interpreter/records/utils/data/values.ts rename to packages/dbml-parse/src/core/global_modules/records/utils/data/values.ts index b6e08015e..d2a78c99d 100644 --- a/packages/dbml-parse/src/core/interpreter/records/utils/data/values.ts +++ b/packages/dbml-parse/src/core/global_modules/records/utils/data/values.ts @@ -1,16 +1,15 @@ +import type { SyntaxNode } from '@/core/types/nodes'; import { - EmptyNode, FunctionExpressionNode, PrefixExpressionNode, - SyntaxNode, -} from '@/core/parser/nodes'; -import { isExpressionAnIdentifierNode } from '@/core/parser/utils'; -import { isExpressionASignedNumberExpression } from '@/core/analyzer/validator/utils'; -import { destructureComplexVariable, extractQuotedStringToken, extractNumericLiteral } from '@/core/analyzer/utils'; + EmptyNode, +} from '@/core/types/nodes'; +import { isExpressionAnIdentifierNode, isExpressionASignedNumberExpression } from '@/core/utils/expression'; +import { destructureComplexVariable, extractQuotedStringToken, extractNumericLiteral } from '@/core/syntax/utils'; import { last } from 'lodash-es'; import { DateTime } from 'luxon'; -export { extractNumericLiteral } from '@/core/analyzer/utils'; +export { extractNumericLiteral } from '@/core/syntax/utils'; // Check if value is a NULL literal/Empty node export function isNullish (value: SyntaxNode): boolean { @@ -22,7 +21,7 @@ export function isNullish (value: SyntaxNode): boolean { } export function isEmptyStringLiteral (value: SyntaxNode): boolean { - return extractQuotedStringToken(value).unwrap_or(undefined) === ''; + return extractQuotedStringToken(value) === ''; } export function isFunctionExpression (value: SyntaxNode): value is FunctionExpressionNode { @@ -51,12 +50,9 @@ export function extractSignedNumber (node: SyntaxNode): number | null { } // Try to extract a numeric value from a syntax node or primitive -// Example: 0, 1, '0', '1', "2", -2, "-2" export function tryExtractNumeric (value: SyntaxNode | number | string | boolean | undefined | null): number | null { - // Handle null/undefined if (value === null || value === undefined) return null; - // Handle primitive types if (typeof value === 'number') return value; if (typeof value === 'string') { const parsed = Number(value); @@ -64,12 +60,10 @@ export function tryExtractNumeric (value: SyntaxNode | number | string | boolean } if (typeof value === 'boolean') return value ? 1 : 0; - // Numeric literal or signed number const num = extractSignedNumber(value); if (num !== null) return num; - // Quoted string containing number: "42", '3.14' - const strValue = extractQuotedStringToken(value).unwrap_or(undefined); + const strValue = extractQuotedStringToken(value); if (strValue !== undefined) { const parsed = Number(strValue); if (!isNaN(parsed)) { @@ -82,36 +76,28 @@ export function tryExtractNumeric (value: SyntaxNode | number | string | boolean // Try to extract an integer value from a syntax node or primitive // Rejects decimal values -// Example: 0, 1, '0', '1', "2", -2, "-2" export function tryExtractInteger (value: SyntaxNode | number | string | boolean | undefined | null): number | null { - // Handle null/undefined if (value === null || value === undefined) return null; - // Handle primitive types if (typeof value === 'number') { - // Reject if it has a decimal part if (!Number.isInteger(value)) return null; return value; } if (typeof value === 'string') { const parsed = Number(value); if (isNaN(parsed)) return null; - // Reject if it has a decimal part if (!Number.isInteger(parsed)) return null; return parsed; } if (typeof value === 'boolean') return value ? 1 : 0; - // Numeric literal or signed number const num = extractSignedNumber(value); if (num !== null) { - // Reject if it has a decimal part if (!Number.isInteger(num)) return null; return num; } - // Quoted string containing number: "42", '3.14' - const strValue = extractQuotedStringToken(value).unwrap_or(undefined); + const strValue = extractQuotedStringToken(value); if (strValue !== undefined) { const parsed = Number(strValue); if (!isNaN(parsed) && Number.isInteger(parsed)) { @@ -126,12 +112,9 @@ export const TRUTHY_VALUES = ['true', 'yes', 'y', 't', '1']; export const FALSY_VALUES = ['false', 'no', 'n', 'f', '0']; // Try to extract a boolean value from a syntax node or primitive -// Example: 't', 'f', 'y', 'n', 'true', 'false', true, false, 'yes', 'no', 1, 0, '1', '0' export function tryExtractBoolean (value: SyntaxNode | number | string | boolean | undefined | null): boolean | null { - // Handle null/undefined if (value === null || value === undefined) return null; - // Handle primitive types if (typeof value === 'boolean') return value; if (typeof value === 'number') { if (value === 0) return false; @@ -157,8 +140,8 @@ export function tryExtractBoolean (value: SyntaxNode | number | string | boolean if (numVal === 0) return false; if (numVal === 1) return true; - // Quoted string: 'true', 'false', 'yes', 'no', 'y', 'n', 't', 'f', '0', '1' - const strValue = extractQuotedStringToken(value)?.unwrap_or('').toLowerCase(); + // Quoted string: 'true', 'false', 'yes', 'no', etc. + const strValue = extractQuotedStringToken(value)?.toLowerCase(); if (strValue) { if (TRUTHY_VALUES.includes(strValue)) return true; if (FALSY_VALUES.includes(strValue)) return false; @@ -170,39 +153,34 @@ export function tryExtractBoolean (value: SyntaxNode | number | string | boolean // Try to extract an enum value from a syntax node or primitive // Either enum references or string are ok export function tryExtractEnum (value: SyntaxNode | string | undefined | null): string | null { - // Handle null/undefined if (value === null || value === undefined) return null; - // Handle primitive string if (typeof value === 'string') return value; // Enum field reference: gender.male - const fragments = destructureComplexVariable(value).unwrap_or(undefined); + const fragments = destructureComplexVariable(value); if (fragments) { return last(fragments)!; } // Quoted string: 'male' - return extractQuotedStringToken(value).unwrap_or(null); + return extractQuotedStringToken(value) ?? null; } // Try to extract a string value from a syntax node or primitive -// Example: "abc", 'abc' export function tryExtractString (value: SyntaxNode | string | boolean | number | undefined | null): string | null { - // Handle null/undefined if (value === null || value === undefined) return null; - // Handle primitive string if (typeof value === 'string') return value; if (typeof value === 'number') return value.toString(); if (typeof value === 'boolean') return value.toString(); - // Quoted string: 'hello', "world" - const res = extractQuotedStringToken(value).unwrap_or(null) ?? tryExtractNumeric(value) ?? tryExtractBoolean(value); // Important: DO NOT move extractNumeric to after extractBoolean, as `1` is extracted as `true` - return res === null ? null : res.toString(); + // Important: DO NOT move extractNumeric to after extractBoolean, as `1` is extracted as `true` + const res = extractQuotedStringToken(value) ?? tryExtractNumeric(value) ?? tryExtractBoolean(value); + return res === null || res === undefined ? null : res.toString(); } -// Supported datetime formats using luxon format tokens (excluding ISO 8601 which is handled separately) +// Supported datetime formats using luxon format tokens const SUPPORTED_DATE_FORMATS = [ 'yyyy-MM-dd', // ISO date: 2023-12-31 'M/d/yyyy', // MM/dd/yyyy: 12/31/2023 or 1/5/2023 @@ -225,26 +203,17 @@ const SUPPORTED_TIME_FORMATS = [ ]; // Try to extract a datetime value from a syntax node or primitive & normalized to ISO 8601 -// Supports: -// - ISO 8601: date (YYYY-MM-DD), time (HH:MM:SS), datetime (YYYY-MM-DDTHH:MM:SS) -// - MM/dd/yyyy: 12/31/2023 -// - d MMM yyyy: 31 Dec 2023 -// - MMM d, yyyy: Dec 31, 2023 -// - yyyy-MM-dd HH:mm:ss: 2023-12-31 23:59:59 -// Example: '2024-01-15', '10:30:00', '2024-01-15T10:30:00Z', '12/31/2023', '31 Dec 2023' export function tryExtractDateTime (value: SyntaxNode | string | undefined | null): string | null { - // Handle null/undefined if (value === null || value === undefined) return null; - const extractedValue = typeof value === 'string' ? value : extractQuotedStringToken(value).unwrap_or(null); + const extractedValue = typeof value === 'string' ? value : extractQuotedStringToken(value); - if (extractedValue === null) return null; + if (extractedValue === null || extractedValue === undefined) return null; - // We prioritize more specific formats, like time-only & date-only before ISO-8601, which includes both date and time + // We prioritize more specific formats, like time-only & date-only before ISO-8601 for (const format of SUPPORTED_TIME_FORMATS) { const dt = DateTime.fromFormat(extractedValue, format, { setZone: true }); if (dt.isValid) { - // https://moment.github.io/luxon/api-docs/index.html#datetimetoisotime return dt.toISOTime({ suppressMilliseconds: true, includeOffset: hasExplicitTimeZone(dt) }); } } @@ -252,7 +221,6 @@ export function tryExtractDateTime (value: SyntaxNode | string | undefined | nul for (const format of SUPPORTED_DATE_FORMATS) { const dt = DateTime.fromFormat(extractedValue, format, { setZone: true }); if (dt.isValid) { - // https://moment.github.io/luxon/api-docs/index.html#datetimetoisodate return dt.toISODate(); } } @@ -260,21 +228,18 @@ export function tryExtractDateTime (value: SyntaxNode | string | undefined | nul for (const format of SUPPORTED_DATETIME_FORMATS) { const dt = DateTime.fromFormat(extractedValue, format, { setZone: true }); if (dt.isValid) { - // https://moment.github.io/luxon/api-docs/index.html#datetimetoiso return dt.toISO({ suppressMilliseconds: true, includeOffset: hasExplicitTimeZone(dt) }); } } const isoDate = DateTime.fromISO(extractedValue, { setZone: true }); if (isoDate.isValid) { - // https://moment.github.io/luxon/api-docs/index.html#datetimetoiso return isoDate.toISO({ suppressMilliseconds: true, includeOffset: hasExplicitTimeZone(isoDate) }); } return null; function hasExplicitTimeZone (dt: DateTime): boolean { - // https://github.com/moment/luxon/blob/master/docs/zones.md#specifying-a-zone return dt.zone.type !== 'system'; } } diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/index.ts b/packages/dbml-parse/src/core/global_modules/records/utils/index.ts similarity index 100% rename from packages/dbml-parse/src/core/interpreter/records/utils/index.ts rename to packages/dbml-parse/src/core/global_modules/records/utils/index.ts diff --git a/packages/dbml-parse/src/core/global_modules/ref/bind.ts b/packages/dbml-parse/src/core/global_modules/ref/bind.ts new file mode 100644 index 000000000..5e9e3c7a4 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/ref/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindRef (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Ref)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/ref/index.ts b/packages/dbml-parse/src/core/global_modules/ref/index.ts new file mode 100644 index 000000000..3d82558f3 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/ref/index.ts @@ -0,0 +1,72 @@ +import { isElementNode, isExpressionAVariableNode, isAccessExpression } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import { AttributeNode, ElementDeclarationNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { PASS_THROUGH, UNHANDLED, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { nodeRefereeOfRefEndpoint } from './utils'; +import { interpretRef } from './interpret'; +import { bindRef } from './bind'; + +// Check if a node is a descendant of a Ref element's body (not its name/alias) +function isInsideRefBody (node: SyntaxNode): boolean { + let current: SyntaxNode | undefined = node.parent; + while (current) { + if (current instanceof ElementDeclarationNode && current.isKind(ElementKind.Ref)) { + // Only if our ancestor path goes through the body, not the name + return current.body?.containsEq(node) ?? false; + } + current = current.parent; + } + return false; +} + +export const refModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Ref)) { + return Report.create(PASS_THROUGH); + } + + return new Report(new NodeSymbol({ + kind: SymbolKind.Ref, + declaration: node, + })); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (!symbol.isKind(SymbolKind.Ref)) { + return Report.create(PASS_THROUGH); + } + + return new Report([]); + }, + + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isExpressionAVariableNode(node) && !isAccessExpression(node)) return Report.create(PASS_THROUGH); + if (!isInsideRefBody(node)) return Report.create(PASS_THROUGH); + + // Skip variables that are inside setting attribute values (e.g. delete: cascade) + if (node.parentOfKind(AttributeNode)) return Report.create(PASS_THROUGH); + + const programNode = compiler.parseFile().getValue().ast; + const globalSymbol = compiler.nodeSymbol(programNode).getValue(); + if (globalSymbol === UNHANDLED) return Report.create(undefined); + + return nodeRefereeOfRefEndpoint(compiler, globalSymbol, node); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Ref)) { + return Report.create(PASS_THROUGH); + } + return new Report([]); + }, + + bind: bindRef, + + interpret: interpretRef, +}; diff --git a/packages/dbml-parse/src/core/global_modules/ref/interpret.ts b/packages/dbml-parse/src/core/global_modules/ref/interpret.ts new file mode 100644 index 000000000..6d1da2f77 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/ref/interpret.ts @@ -0,0 +1,194 @@ +import { ElementKind } from '@/core/types/keywords'; +import { FunctionApplicationNode, IdentiferStreamNode, PrimaryExpressionNode, LiteralNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { extractVariableFromExpression, isBinaryRelationship, destructureComplexVariableTuple, extractQuotedStringToken } from '@/core/syntax/utils'; +import { getTokenPosition } from '../utils'; +import { Ref, RefEndpoint, schemaFrom } from '@/core/types/schemaJson'; +import type { RefEndpointPair, RelationCardinality } from '@/core/types/schemaJson'; +import type { CompileError } from '@/core/types/errors'; +import { extractStringFromIdentifierStream, getBody, isElementNode } from '@/core/utils/expression'; +import { SyntaxTokenKind } from '@/core/types/tokens'; + +function extractColorValue (node: unknown): string | undefined { + if (node instanceof PrimaryExpressionNode && node.expression instanceof LiteralNode && node.expression.literal?.kind === SyntaxTokenKind.COLOR_LITERAL) { + return node.expression.literal.value; + } + return undefined; +} + +function resolveRelationCardinalities (op: string): [RelationCardinality, RelationCardinality] | undefined { + switch (op) { + case '>': return ['*', '1']; + case '<': return ['1', '*']; + case '-': return ['1', '1']; + case '<>': return ['*', '*']; + default: return undefined; + } +} + +export function interpretRef (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Ref)) return Report.create(PASS_THROUGH); + + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const refName = name?.at(-1) ?? null; + + const token = getTokenPosition(node); + const errors: CompileError[] = []; + + // The ref body contains the relationship expression (e.g. `users.id > posts.user_id`) + const bodyFields = getBody(node); + + // Extract ref-level settings: onDelete, onUpdate, color + const settingsResult = compiler.settings(node); + errors.push(...settingsResult.getErrors()); + + let onDelete: string | undefined; + let onUpdate: string | undefined; + let color: string | undefined; + + if (!settingsResult.hasValue(UNHANDLED)) { + const settings = settingsResult.getValue(); + if (settings.get('delete')?.[0]?.value) { + const deleteNode = settings.get('delete')![0].value; + if (deleteNode instanceof IdentiferStreamNode) { + onDelete = extractStringFromIdentifierStream(deleteNode) ?? extractVariableFromExpression(deleteNode); + } else { + onDelete = extractVariableFromExpression(deleteNode); + } + } + if (settings.get('update')?.[0]?.value) { + const updateNode = settings.get('update')![0].value; + if (updateNode instanceof IdentiferStreamNode) { + onUpdate = extractStringFromIdentifierStream(updateNode) ?? extractVariableFromExpression(updateNode); + } else { + onUpdate = extractVariableFromExpression(updateNode); + } + } + if (settings.get('color')?.[0]?.value) { + color = extractQuotedStringToken(settings.get('color')![0].value) ?? extractColorValue(settings.get('color')![0].value); + } + } + + // Parse the binary relationship expression into a pair of endpoints with cardinalities + let endpoints: RefEndpointPair | undefined; + + for (const field of bodyFields) { + const expr = field instanceof FunctionApplicationNode ? field.callee : undefined; + if (!expr) continue; + + if (isBinaryRelationship(expr)) { + const op = expr.op?.value; + const cardinalities = op ? resolveRelationCardinalities(op) : undefined; + if (!cardinalities) continue; + + const leftTuple = destructureComplexVariableTuple(expr.leftExpression); + const rightTuple = destructureComplexVariableTuple(expr.rightExpression); + + if (leftTuple && rightTuple) { + const buildEndpoint = (tuple: NonNullable>, relation: RelationCardinality, sideExpr: SyntaxNode): RefEndpoint => { + const vars = tuple.variables.map((v) => v.expression.variable?.value ?? ''); + let tableName: string; + let schemaName: string | null; + let fieldNames: string[]; + + if (tuple.tupleElements.length > 0) { + // Tuple form: table.(col1, col2) or schema.table.(col1, col2) + tableName = vars.at(-1) ?? ''; + schemaName = vars.length > 1 ? vars.slice(0, -1).join('.') : null; + fieldNames = tuple.tupleElements.map((e) => e.expression.variable?.value ?? ''); + } else { + // Non-tuple form: table.column or schema.table.column + // Last var is column, second-to-last is table, rest is schema + fieldNames = vars.length > 0 ? [vars.at(-1)!] : []; + tableName = vars.length > 1 ? vars.at(-2)! : ''; + schemaName = vars.length > 2 ? vars.slice(0, -2).join('.') : null; + } + + return schemaFrom(RefEndpoint, { + schemaName, + tableName, + fieldNames, + relation, + token: getTokenPosition(sideExpr), + }); + }; + + endpoints = [ + buildEndpoint(leftTuple, cardinalities[0], expr.leftExpression), + buildEndpoint(rightTuple, cardinalities[1], expr.rightExpression), + ]; + } + break; + } + } + + // Settings can also appear inline on the relationship line; merge them as fallbacks + for (const field of bodyFields) { + if (!(field instanceof FunctionApplicationNode)) continue; + const fieldSettingsResult = compiler.settings(field); + errors.push(...fieldSettingsResult.getErrors()); + + if (!fieldSettingsResult.hasValue(UNHANDLED)) { + const fieldSettings = fieldSettingsResult.getValue(); + + if (!onDelete && fieldSettings.get('delete')?.[0]?.value) { + const deleteNode = fieldSettings.get('delete')![0].value; + if (deleteNode instanceof IdentiferStreamNode) { + onDelete = extractStringFromIdentifierStream(deleteNode) ?? extractVariableFromExpression(deleteNode); + } else { + onDelete = extractVariableFromExpression(deleteNode); + } + } + if (!onUpdate && fieldSettings.get('update')?.[0]?.value) { + const updateNode = fieldSettings.get('update')![0].value; + if (updateNode instanceof IdentiferStreamNode) { + onUpdate = extractStringFromIdentifierStream(updateNode) ?? extractVariableFromExpression(updateNode); + } else { + onUpdate = extractVariableFromExpression(updateNode); + } + } + if (!color && fieldSettings.get('color')?.[0]?.value) { + color = extractQuotedStringToken(fieldSettings.get('color')![0].value) ?? extractColorValue(fieldSettings.get('color')![0].value); + } + } + } + + // Emit a stub ref with empty endpoints when the relationship expression couldn't be parsed + if (!endpoints) { + return new Report(schemaFrom(Ref, { + schemaName: null, + name: refName, + endpoints: [schemaFrom(RefEndpoint, { + schemaName: null, + tableName: '', + fieldNames: [], + relation: '1', + token, + }), schemaFrom(RefEndpoint, { + schemaName: null, + tableName: '', + fieldNames: [], + relation: '1', + token, + })], + token, + ...(onDelete && { onDelete }), + ...(onUpdate && { onUpdate }), + ...(color && { color }), + }), errors); + } + + return new Report(schemaFrom(Ref, { + schemaName: null, + name: refName, + endpoints, + token, + ...(onDelete && { onDelete }), + ...(onUpdate && { onUpdate }), + ...(color && { color }), + }), errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/ref/utils.ts b/packages/dbml-parse/src/core/global_modules/ref/utils.ts new file mode 100644 index 000000000..1a95eb9b6 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/ref/utils.ts @@ -0,0 +1,84 @@ +import type Compiler from '@/compiler'; +import { InfixExpressionNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import Report from '@/core/types/report'; +import { DEFAULT_SCHEMA_NAME, UNHANDLED } from '@/constants'; +import { lookupMember, nodeRefereeOfLeftExpression } from '../utils'; +import { NodeSymbol, SymbolKind, SchemaSymbol } from '@/core/types/symbols'; +import { isAccessExpression, isExpressionAVariableNode } from '@/core/utils/expression'; +import { extractVarNameFromPrimaryVariable } from '@/core/syntax/utils'; + +// Find the default 'public' schema symbol for binding lookups. +function getDefaultSchemaSymbol (compiler: Compiler, globalSymbol: NodeSymbol): NodeSymbol | undefined { + const members = compiler.symbolMembers(globalSymbol); + if (members.hasValue(UNHANDLED)) return undefined; + + return members.getValue().find((m: NodeSymbol) => { + if (!m.isKind(SymbolKind.Schema) || !m.declaration) return false; + const fn = compiler.fullname(m.declaration); + return !fn.hasValue(UNHANDLED) && fn.getValue()?.at(-1) === DEFAULT_SCHEMA_NAME; + }); +} + +function findTableByAlias (compiler: Compiler, parentSymbol: NodeSymbol, alias: string): NodeSymbol | undefined { + const members = compiler.symbolMembers(parentSymbol); + if (members.hasValue(UNHANDLED)) return undefined; + for (const m of members.getValue()) { + if (!m.isKind(SymbolKind.Table) || !m.declaration) continue; + const aliasResult = compiler.alias(m.declaration); + if (!aliasResult.hasValue(UNHANDLED) && aliasResult.getValue() === alias) { + return m; + } + } + return undefined; +} + +// Ref endpoint: table.column or schema.table.column +// Always report errors, never ignore not found +export function nodeRefereeOfRefEndpoint (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + // Standalone variable - ignore not found since it may be a tuple element or ambiguous ref + if (!isAccessExpression(node.parent)) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Column], ignoreNotFound: true, errorNode: node }); + } + + // Right side of access expression - resolve via left sibling + const left = nodeRefereeOfLeftExpression(compiler, node); + if (left) { + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Table, SymbolKind.Schema], errorNode: node }); + } + if (left.isKind(SymbolKind.Table)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Column], errorNode: node }); + } + return new Report(undefined); + } + + // Left side of access expression - look up as Table or Schema + const parent = node.parent as InfixExpressionNode; + if (parent.leftExpression === node) { + // If parent is also left side of another access, this is a schema + if (isAccessExpression(parent.parent) && (parent.parent as InfixExpressionNode).leftExpression === parent) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Schema], errorNode: node }); + } + // Otherwise look up as Table (in public schema or program scope), with alias fallback + const schemaSymbol = getDefaultSchemaSymbol(compiler, globalSymbol); + if (schemaSymbol) { + const byName = lookupMember(compiler, schemaSymbol, name, { kinds: [SymbolKind.Table], ignoreNotFound: true, errorNode: node }); + if (byName.getValue()) return byName; + // Try alias lookup + const byAlias = findTableByAlias(compiler, schemaSymbol, name); + if (byAlias) return new Report(byAlias); + } + const byName = lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Table], ignoreNotFound: true, errorNode: node }); + if (byName.getValue()) return byName; + const byAlias = findTableByAlias(compiler, globalSymbol, name); + if (byAlias) return new Report(byAlias); + // Not found - report error + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Table], errorNode: node }); + } + + return new Report(undefined); +} diff --git a/packages/dbml-parse/src/core/global_modules/schema/bind.ts b/packages/dbml-parse/src/core/global_modules/schema/bind.ts new file mode 100644 index 000000000..540da9391 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/schema/bind.ts @@ -0,0 +1,8 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; + +export function bindSchema (compiler: Compiler, node: SyntaxNode): Report { + return Report.create(PASS_THROUGH); +} diff --git a/packages/dbml-parse/src/core/global_modules/schema/index.ts b/packages/dbml-parse/src/core/global_modules/schema/index.ts new file mode 100644 index 000000000..f8d15cbc9 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/schema/index.ts @@ -0,0 +1,58 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SchemaSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { DEFAULT_SCHEMA_NAME, PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { bindSchema } from './bind'; +import { interpretSchema } from './interpret'; + +export const schemaModule: Module = { + // Schemas don't have their own AST nodes - they are synthesized + // from dotted names (e.g. `auth.users` creates schema `auth`). + // nodeSymbol is not used for schemas; they are created via symbolMembers on Program. + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { return Report.create(PASS_THROUGH); }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { return Report.create(PASS_THROUGH); }, + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { return Report.create(PASS_THROUGH); }, + + bind: bindSchema, + + interpret: interpretSchema, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (!symbol.isKind(SymbolKind.Schema)) return Report.create(PASS_THROUGH); + + const schemaName = symbol instanceof SchemaSymbol ? symbol.name : undefined; + if (!schemaName) return new Report([]); + + // Get the program symbol to access direct members + const programSymbol = compiler.nodeSymbol(compiler.parseFile().getValue().ast); + if (programSymbol.hasValue(UNHANDLED)) return new Report([]); + + const allTopLevel = compiler.symbolMembers(programSymbol.getValue()); + if (allTopLevel.hasValue(UNHANDLED)) return new Report([]); + + // Filter direct element members (not SchemaSymbols) that belong to this schema + const members = allTopLevel.getValue().filter((member: NodeSymbol) => { + if (member instanceof SchemaSymbol) return false; + if (!member.declaration) return false; + const memberName = compiler.fullname(member.declaration); + if (memberName.hasValue(UNHANDLED)) return false; + const fullname = memberName.getValue(); + if (!fullname || fullname.length === 0) return false; + + if (schemaName === DEFAULT_SCHEMA_NAME) { + // 'public' schema: unqualified names OR explicitly qualified with 'public.' + return fullname.length === 1 || (fullname.length > 1 && fullname[0] === DEFAULT_SCHEMA_NAME); + } else { + // Named schema: elements whose first name segment matches + return fullname.length > 1 && fullname[0] === schemaName; + } + }); + + return new Report(members); + }, + +}; diff --git a/packages/dbml-parse/src/core/global_modules/schema/interpret.ts b/packages/dbml-parse/src/core/global_modules/schema/interpret.ts new file mode 100644 index 000000000..dba229cb1 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/schema/interpret.ts @@ -0,0 +1,6 @@ +import Report from '@/core/types/report'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import type Compiler from '@/compiler/index'; + +export function interpretSchema (compiler: Compiler, node: SyntaxNode): Report { return Report.create(PASS_THROUGH); } diff --git a/packages/dbml-parse/src/core/global_modules/schema/utils.ts b/packages/dbml-parse/src/core/global_modules/schema/utils.ts new file mode 100644 index 000000000..70ca3651a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/schema/utils.ts @@ -0,0 +1 @@ +// Module-specific utilities diff --git a/packages/dbml-parse/src/core/global_modules/stickyNote/bind.ts b/packages/dbml-parse/src/core/global_modules/stickyNote/bind.ts new file mode 100644 index 000000000..7bc89e55b --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/stickyNote/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindNote (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Note)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/stickyNote/index.ts b/packages/dbml-parse/src/core/global_modules/stickyNote/index.ts new file mode 100644 index 000000000..b8fa24fa7 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/stickyNote/index.ts @@ -0,0 +1,44 @@ +import { isElementNode } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { interpretNote } from './interpret'; +import { bindNote } from './bind'; + +export const noteModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Note)) { + return Report.create(PASS_THROUGH); + } + + return new Report(new NodeSymbol({ + kind: SymbolKind.Note, + declaration: node, + })); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (!symbol.isKind(SymbolKind.Note)) { + return Report.create(PASS_THROUGH); + } + + return new Report([]); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Note)) { + return Report.create(PASS_THROUGH); + } + return new Report([]); + }, + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { return Report.create(PASS_THROUGH); }, + + bind: bindNote, + + interpret: interpretNote, +}; diff --git a/packages/dbml-parse/src/core/global_modules/stickyNote/interpret.ts b/packages/dbml-parse/src/core/global_modules/stickyNote/interpret.ts new file mode 100644 index 000000000..34e675bc0 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/stickyNote/interpret.ts @@ -0,0 +1,47 @@ +import { ElementKind } from '@/core/types/keywords'; +import { FunctionApplicationNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { extractQuotedStringToken } from '@/core/syntax/utils'; +import { getTokenPosition, normalizeNoteContent } from '../utils'; +import { Note, schemaFrom } from '@/core/types/schemaJson'; +import { getBody, isElementNode } from '@/core/utils/expression'; + +export function interpretNote (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Note)) return Report.create(PASS_THROUGH); + + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const noteName = name?.at(-1) ?? ''; + + const token = getTokenPosition(node); + + // Try extracting the note text from block body fields first + let content = ''; + const bodyFields = getBody(node); + for (const field of bodyFields) { + if (field instanceof FunctionApplicationNode && field.callee) { + const extracted = extractQuotedStringToken(field.callee); + if (extracted !== undefined) { + content = extracted; + break; + } + } + } + + // Fall back to colon-syntax form (e.g. `Note: 'text'`) + if (!content && node.body && node.body instanceof FunctionApplicationNode) { + const extracted = extractQuotedStringToken(node.body.callee); + if (extracted !== undefined) { + content = extracted; + } + } + + return new Report(schemaFrom(Note, { + name: noteName, + content: normalizeNoteContent(content), + token, + })); +} diff --git a/packages/dbml-parse/src/core/global_modules/stickyNote/utils.ts b/packages/dbml-parse/src/core/global_modules/stickyNote/utils.ts new file mode 100644 index 000000000..70ca3651a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/stickyNote/utils.ts @@ -0,0 +1 @@ +// Module-specific utilities diff --git a/packages/dbml-parse/src/core/global_modules/table/bind.ts b/packages/dbml-parse/src/core/global_modules/table/bind.ts new file mode 100644 index 000000000..ca81a0320 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/table/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindTable (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Table) && !isElementFieldNode(node, ElementKind.Table)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/table/index.ts b/packages/dbml-parse/src/core/global_modules/table/index.ts new file mode 100644 index 000000000..0e3fe7b6b --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/table/index.ts @@ -0,0 +1,148 @@ +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { ElementDeclarationNode, FunctionApplicationNode, PrefixExpressionNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import type { SchemaElement } from '@/core/types/schemaJson'; +import { extractVariableFromExpression } from '@/core/syntax/utils'; +import { getNodeMemberSymbols, getSymbolDirectMembers, lookupMember } from '../utils'; +import { isValidPartialInjection } from '@/core/utils/validate'; +import { getBody, isElementNode, isExpressionAVariableNode, isAccessExpression, isInsideElementBody, isWithinNthArgOfField, isInsideSettingValue } from '@/core/utils/expression'; +import { interpretTable, interpretTableField } from './interpret'; +import { bindTable } from './bind'; +import { nodeRefereeOfEnumType, nodeRefereeOfInlineRef, nodeRefereeOfEnumDefault } from './utils'; + +export const tableModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Table)) { + return new Report(new NodeSymbol({ + kind: SymbolKind.Table, + declaration: node, + })); + } + if (isInsideElementBody(node, ElementKind.Table)) { + return new Report(new NodeSymbol({ kind: SymbolKind.Column, declaration: node })); + } + return Report.create(PASS_THROUGH); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (!symbol.isKind(SymbolKind.Table)) { + return Report.create(PASS_THROUGH); + } + + // Start with the table's own direct children (columns, sub-elements) + const directMembers = getSymbolDirectMembers(compiler, symbol); + const members = [...directMembers.getValue()]; + const errors = [...directMembers.getErrors()]; + + // Detect partial injections (~partial_name) and merge in their columns + const body = symbol.declaration instanceof ElementDeclarationNode ? getBody(symbol.declaration) : []; + for (const child of body) { + if (!(child instanceof FunctionApplicationNode)) continue; + if (!isValidPartialInjection(child.callee)) continue; + + // Extract the partial name from ~partial_name + const partialNameNode = (child.callee as PrefixExpressionNode).expression; + const partialName = extractVariableFromExpression(partialNameNode); + if (!partialName) continue; + + // Look up the TablePartial symbol among direct program elements + const ast = compiler.parseFile().getValue().ast; + const programSymbol = compiler.nodeSymbol(ast); + if (programSymbol.hasValue(UNHANDLED)) continue; + // Use getSymbolDirectMembers to avoid cycle through schema symbolMembers + const directMembers = getSymbolDirectMembers(compiler, programSymbol.getValue()); + if (directMembers.hasValue(UNHANDLED)) continue; + + const partialSymbol = directMembers.getValue().find((m: NodeSymbol) => { + if (!m.isKind(SymbolKind.TablePartial) || !m.declaration) return false; + const fn = compiler.fullname(m.declaration); + if (fn.hasValue(UNHANDLED)) return false; + return fn.getValue()?.at(-1) === partialName; + }); + + if (!partialSymbol) continue; + + // Merge the partial's column members into this table's members + const partialMembers = compiler.symbolMembers(partialSymbol); + if (!partialMembers.hasValue(UNHANDLED)) { + members.push(...partialMembers.getValue()); + errors.push(...partialMembers.getErrors()); + } + } + + return new Report(members, errors); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Table)) { + return getNodeMemberSymbols(compiler, node); + } + if (isInsideElementBody(node, ElementKind.Table)) { + return new Report([]); + } + return Report.create(PASS_THROUGH); + }, + + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isExpressionAVariableNode(node) && !isAccessExpression(node)) { + return Report.create(PASS_THROUGH); + } + if (!isInsideElementBody(node, ElementKind.Table)) { + return Report.create(PASS_THROUGH); + } + + const programNode = compiler.parseFile().getValue().ast; + const globalSymbol = compiler.nodeSymbol(programNode).getValue(); + + if (globalSymbol === UNHANDLED) { + return Report.create(undefined); + } + + // Case 0: Partial injection (~partial_name) - resolve as TablePartial + if (isExpressionAVariableNode(node) + && node.parent instanceof PrefixExpressionNode + && node.parent.op?.value === '~') { + const name = node.expression?.variable?.value ?? ''; + // Look up in public schema since TablePartials are top-level elements + const members = compiler.symbolMembers(globalSymbol); + if (!members.hasValue(UNHANDLED)) { + const publicSchema = members.getValue().find((m: any) => m.name === 'public' && m.isKind(SymbolKind.Schema)); + if (publicSchema) { + return lookupMember(compiler, publicSchema, name, { kinds: [SymbolKind.TablePartial], errorNode: node }); + } + } + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.TablePartial], errorNode: node }); + } + + // Case 1: Column's enum type + if (isWithinNthArgOfField(node, 1)) { + return nodeRefereeOfEnumType(compiler, globalSymbol, node); + } + + // Case 2: Column's inline ref + if (isInsideSettingValue(node, SettingName.Ref)) { + return nodeRefereeOfInlineRef(compiler, globalSymbol, node); + } + + // Case 3: Column's default value being an enum value + return nodeRefereeOfEnumDefault(compiler, globalSymbol, node); + }, + + bind: bindTable, + + interpret (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Table)) { + return interpretTable(compiler, node); + } + if (isInsideElementBody(node, ElementKind.Table)) { + return interpretTableField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/global_modules/table/interpret.ts b/packages/dbml-parse/src/core/global_modules/table/interpret.ts new file mode 100644 index 000000000..e3d0e7592 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/table/interpret.ts @@ -0,0 +1,405 @@ +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, FunctionExpressionNode, PrefixExpressionNode, CallExpressionNode, ArrayNode, ListExpressionNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { destructureComplexVariable, extractVariableFromExpression, extractQuotedStringToken, isBinaryRelationship, destructureComplexVariableTuple } from '@/core/syntax/utils'; +import { getTokenPosition, normalizeNoteContent } from '../utils'; +import { Column, Check, Index, InlineRef, Table, TablePartialInjection, TableRecord, schemaFrom } from '@/core/types/schemaJson'; +import type { SchemaElement, TokenPosition, ColumnType } from '@/core/types/schemaJson'; +import { isValidPartialInjection } from '@/core/utils/validate'; +import { isExpressionAQuotedString, isExpressionAVariableNode, isExpressionASignedNumberExpression, getNumberTextFromExpression, parseNumber, isRelationshipOp, isElementNode, isInsideElementBody } from '@/core/utils/expression'; +import { PrimaryExpressionNode, LiteralNode } from '@/core/types/nodes'; +import { SyntaxTokenKind } from '@/core/types/tokens'; +import type { CompileError } from '@/core/types/errors'; + +function extractColorValue (node: unknown): string | undefined { + if (node instanceof PrimaryExpressionNode && node.expression instanceof LiteralNode && node.expression.literal?.kind === SyntaxTokenKind.COLOR_LITERAL) { + return node.expression.literal.value; + } + return undefined; +} + +export function interpretTable (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Table)) return Report.create(PASS_THROUGH); + + // Derive schema-qualified name from the dotted name expression + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const lastSegment = name?.at(-1) ?? ''; + const schemaSegments = name?.slice(0, -1) ?? []; + + const aliasResult = compiler.alias(node); + const aliasValue = !aliasResult.hasValue(UNHANDLED) ? aliasResult.getValue() : undefined; + + const token = getTokenPosition(node); + + // Fall back to an empty table if symbol resolution fails + const symbolResult = compiler.nodeSymbol(node); + if (symbolResult.hasValue(UNHANDLED)) { + return new Report
(schemaFrom(Table, { + name: lastSegment, + schemaName: schemaSegments.join('.') || null, + alias: aliasValue || null, + fields: [], + checks: [], + partials: [], + indexes: [], + token, + })); + } + + // Recursively interpret each column member + const membersResult = compiler.symbolMembers(symbolResult.getValue()); + const members = !membersResult.hasValue(UNHANDLED) ? membersResult.getValue() : []; + + // Interpret columns from direct members (only those declared directly in this table's body) + const fields: Column[] = []; + const errors: CompileError[] = []; + const warnings: import('@/core/types/errors').CompileWarning[] = []; + for (const member of members) { + if (!member.declaration) continue; + // Skip columns from partial injections - they belong to the partial, not this table + const parentElement = member.declaration.parentElement; + if (parentElement instanceof ElementDeclarationNode && parentElement !== node) continue; + // Skip partial injection references (~partial_name) - they are not columns + if (member.declaration instanceof FunctionApplicationNode && isValidPartialInjection(member.declaration.callee)) continue; + + const fieldResult = compiler.interpret(member.declaration); + if (!fieldResult.hasValue(UNHANDLED)) { + const field = fieldResult.getValue(); + if (field instanceof Column) fields.push(field); + errors.push(...fieldResult.getErrors()); + } + } + + // Process sub-elements: checks, indexes, partial injections, nested records, body-level notes + const checks: Check[] = []; + const indexes: Index[] = []; + const partials: TablePartialInjection[] = []; + const nestedRecords: TableRecord[] = []; + let bodyNote: { value: string; token: TokenPosition } | undefined; + + const body = node.body; + if (body instanceof BlockExpressionNode) { + const subs = body.body.filter((e): e is ElementDeclarationNode => e instanceof ElementDeclarationNode); + for (const sub of subs) { + // Detect body-level Note sub-elements + if (isElementNode(sub, ElementKind.Note)) { + let noteContent: string | undefined; + // Colon form: `Note: 'string'` + if (sub.bodyColon && sub.body instanceof FunctionApplicationNode) { + noteContent = extractQuotedStringToken(sub.body.callee); + } + // Block form: `Note { 'string' }` + if (!sub.bodyColon && sub.body instanceof BlockExpressionNode) { + const firstField = sub.body.body[0]; + if (firstField instanceof FunctionApplicationNode) { + noteContent = extractQuotedStringToken(firstField.callee); + } + } + if (noteContent !== undefined) { + bodyNote = { + value: normalizeNoteContent(noteContent), + token: getTokenPosition(sub), + }; + continue; + } + } + + const result = compiler.interpret(sub); + if (result.hasValue(UNHANDLED)) continue; + const value = result.getValue(); + errors.push(...result.getErrors()); + warnings.push(...result.getWarnings()); + if (value instanceof TableRecord) { + nestedRecords.push(value); + } else if (Array.isArray(value)) { + for (const v of value) { + if (v instanceof Check) checks.push(v); + else if (v instanceof Index) indexes.push(v); + else if (v instanceof TableRecord) nestedRecords.push(v); + } + } + } + + // Detect partial injections (~partial_name) and record their order among all fields + let order = 0; + for (const field of body.body) { + if (!(field instanceof FunctionApplicationNode)) { continue; } + if (isValidPartialInjection(field.callee)) { + const partialName = extractVariableFromExpression(field.callee.expression) ?? ''; + partials.push(schemaFrom(TablePartialInjection, { + name: partialName, + order, + token: getTokenPosition(field), + })); + } + order++; + } + } + + // Extract table-level settings (headerColor, note) + const settingsResult = compiler.settings(node); + errors.push(...settingsResult.getErrors()); + + let headerColor: string | undefined; + let note: { value: string; token: TokenPosition } | undefined; + + if (!settingsResult.hasValue(UNHANDLED)) { + const settings = settingsResult.getValue(); + if (settings.get(SettingName.HeaderColor)?.[0]?.value) { + const colorNode = settings.get(SettingName.HeaderColor)![0].value; + headerColor = extractColorValue(colorNode); + } + + if (settings.get(SettingName.Note)?.[0]?.value) { + const noteValue = extractQuotedStringToken(settings.get(SettingName.Note)![0].value); + if (noteValue !== undefined) { + note = { + value: normalizeNoteContent(noteValue), + token: getTokenPosition(settings.get(SettingName.Note)![0]), + }; + } + } + } + + // Use body-level note as fallback if no settings-level note + if (!note && bodyNote) { + note = bodyNote; + } + + // Fill in empty tableNames in inline refs with the current table name + for (const field of fields) { + for (const ref of field.inline_refs) { + if (!ref.tableName && ref.fieldNames.length > 0) { + ref.tableName = lastSegment; + ref.schemaName = schemaSegments.join('.') || null; + } + } + } + + const table = schemaFrom(Table, { + name: lastSegment, + schemaName: schemaSegments.join('.') || null, + alias: aliasValue ?? null, + fields, + checks, + partials, + indexes, + token, + ...(headerColor && { headerColor }), + ...(note && { note }), + }); + + // If there are nested records, return them alongside the table + if (nestedRecords.length > 0) { + return new Report( + [table, ...nestedRecords], + errors, + warnings, + ); + } + + return new Report(table, errors, warnings); +} + +export function interpretTableField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!(node instanceof FunctionApplicationNode) || !isInsideElementBody(node, ElementKind.Table)) return Report.create(PASS_THROUGH); + + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const fieldName = name?.at(-1) ?? ''; + + const token = getTokenPosition(node); + const errors: CompileError[] = []; + + // Parse column type, reproducing master's processColumnType logic + let rawTypeNode: SyntaxNode | undefined = node.args[0]; + let columnType: ColumnType = { schemaName: null, type_name: '', args: null }; + + if (rawTypeNode) { + let typeSuffix = ''; + let typeArgs: string | null = null; + + // First pass: extract top-level call args (e.g. varchar(255)) + if (rawTypeNode instanceof CallExpressionNode && rawTypeNode.argumentList) { + typeArgs = rawTypeNode.argumentList.elementList.map((e) => { + if (isExpressionASignedNumberExpression(e)) return getNumberTextFromExpression(e); + if (isExpressionAQuotedString(e)) return extractQuotedStringToken(e) ?? ''; + if (isExpressionAVariableNode(e)) return e.expression.variable.value; + return ''; + }).join(','); + typeSuffix = `(${typeArgs})`; + rawTypeNode = rawTypeNode.callee; + } + + // Remaining passes: handle nested calls and array brackets + while (rawTypeNode instanceof CallExpressionNode || rawTypeNode instanceof ArrayNode) { + if (rawTypeNode instanceof CallExpressionNode) { + const args = rawTypeNode.argumentList?.elementList.map((e) => { + if (isExpressionASignedNumberExpression(e)) return getNumberTextFromExpression(e); + if (isExpressionAQuotedString(e)) return extractQuotedStringToken(e) ?? ''; + if (isExpressionAVariableNode(e)) return e.expression.variable.value; + return ''; + }).join(',') ?? ''; + typeSuffix = `(${args})${typeSuffix}`; + rawTypeNode = rawTypeNode.callee; + } else { + const indexer = `[${rawTypeNode.indexer?.elementList.map((e: any) => e?.name?.expression?.literal?.value ?? '').join(',') ?? ''}]`; + typeSuffix = `${indexer}${typeSuffix}`; + rawTypeNode = rawTypeNode.array; + } + } + + const typeFragments = rawTypeNode ? destructureComplexVariable(rawTypeNode) : undefined; + if (typeFragments && typeFragments.length > 0) { + const typeName = typeFragments.at(-1) ?? ''; + const typeSchema = typeFragments.length > 1 ? typeFragments.slice(0, -1).join('.') : null; + columnType = { + schemaName: typeSchema, + type_name: `${typeName}${typeSuffix}`, + args: typeArgs, + }; + } + } + + // Extract column-level settings (pk, unique, not null, default, note, inline refs) + const settingsResult = compiler.settings(node); + errors.push(...settingsResult.getErrors()); + + let pk: boolean | undefined; + let unique: boolean | undefined; + let not_null: boolean | undefined; + let increment: boolean | undefined; + let dbdefault: Column['dbdefault'] | undefined; + let note: { value: string; token: TokenPosition } | undefined; + const inline_refs: InlineRef[] = []; + const columnChecks: Check[] = []; + + if (!settingsResult.hasValue(UNHANDLED)) { + const settings = settingsResult.getValue(); + + if (settings.get(SettingName.PK) || settings.get(SettingName.PrimaryKey)) { + pk = true; + } + if (settings.get(SettingName.Unique)) { + unique = true; + } + if (settings.get(SettingName.NotNull)) { + not_null = true; + } + if (settings.get(SettingName.Null)) { + not_null = false; + } + if (settings.get(SettingName.Increment)) { + increment = true; + } + if (settings.get(SettingName.Default)?.[0]?.value) { + const defaultNode = settings.get(SettingName.Default)?.[0].value; + if (isExpressionAQuotedString(defaultNode)) { + dbdefault = { type: 'string', value: extractQuotedStringToken(defaultNode) ?? '' }; + } else if (isExpressionASignedNumberExpression(defaultNode)) { + dbdefault = { type: 'number', value: parseNumber(defaultNode) }; + } else if (defaultNode instanceof FunctionExpressionNode) { + dbdefault = { type: 'expression', value: defaultNode.value?.value ?? '' }; + } else if (isExpressionAVariableNode(defaultNode)) { + const val = defaultNode.expression.variable.value.toLowerCase(); + dbdefault = { type: 'boolean', value: val }; + } else { + // Enum default value: schema.enum.field or enum.field + // Extract the last segment as a string value + const fragments = destructureComplexVariable(defaultNode); + if (fragments && fragments.length > 0) { + dbdefault = { type: 'string', value: fragments.at(-1) ?? '' }; + } + } + } + if (settings.get(SettingName.Note)?.[0]?.value) { + const noteValue = extractQuotedStringToken(settings.get(SettingName.Note)![0].value); + if (noteValue !== undefined) { + note = { + value: normalizeNoteContent(noteValue), + token: getTokenPosition(settings.get(SettingName.Note)![0]), + }; + } + } + + // Parse column-level check constraints (e.g. `check: \`LEN(name) > 0\``) + if (settings.get(SettingName.Check)) { + for (const checkAttr of settings.get(SettingName.Check)!) { + if (!checkAttr.value) continue; + if (checkAttr.value instanceof FunctionExpressionNode) { + const expression = checkAttr.value.value?.value ?? ''; + columnChecks.push(schemaFrom(Check, { + expression, + token: getTokenPosition(checkAttr), + })); + } + } + } + + // Parse inline ref declarations (e.g. `ref: > other_table.id`) into endpoint objects + if (settings.get(SettingName.Ref)) { + for (const refAttr of settings.get(SettingName.Ref)!) { + if (!refAttr.value) continue; + if (isBinaryRelationship(refAttr.value)) { + const op = refAttr.value.op?.value; + const rightTuple = destructureComplexVariableTuple(refAttr.value.rightExpression); + if (rightTuple && op && isRelationshipOp(op)) { + const vars = rightTuple.variables; + const tableName = vars.map((v) => v.expression.variable?.value ?? '').at(-1) ?? ''; + const schemaName = vars.length > 1 ? vars.slice(0, -1).map((v) => v.expression.variable?.value ?? '').join('.') : null; + const fieldNames = rightTuple.tupleElements.length > 0 + ? rightTuple.tupleElements.map((e) => e.expression.variable?.value ?? '') + : []; + inline_refs.push(new InlineRef(getTokenPosition(refAttr), schemaName, tableName, fieldNames, op)); + } + } else if (refAttr.value instanceof PrefixExpressionNode && isRelationshipOp(refAttr.value.op?.value)) { + // Handle prefix form: `ref: > users.id` (PrefixExpressionNode with op > < - <>) + const op = refAttr.value.op!.value as '>' | '<' | '-' | '<>'; + const targetTuple = destructureComplexVariableTuple(refAttr.value.expression); + if (targetTuple) { + const vars = targetTuple.variables.map((v) => v.expression.variable?.value ?? ''); + let tableName: string; + let schemaName: string | null; + let fieldNames: string[]; + + if (targetTuple.tupleElements.length > 0) { + tableName = vars.at(-1) ?? ''; + schemaName = vars.length > 1 ? vars.slice(0, -1).join('.') : null; + fieldNames = targetTuple.tupleElements.map((e) => e.expression.variable?.value ?? ''); + } else { + // table.column or schema.table.column + fieldNames = vars.length > 0 ? [vars.at(-1)!] : []; + tableName = vars.length > 1 ? vars.at(-2)! : ''; + schemaName = vars.length > 2 ? vars.slice(0, -2).join('.') : null; + } + inline_refs.push(new InlineRef(getTokenPosition(refAttr), schemaName, tableName, fieldNames, op)); + } + } + } + } + } + + // Detect if column uses [settings] bracket syntax (vs inline PK/unique keywords) + const hasBracketSettings = node.args.some((a) => a instanceof ListExpressionNode); + + const result = schemaFrom(Column, { + name: fieldName, + type: columnType, + inline_refs, + checks: columnChecks, + token, + ...(pk !== undefined && { pk }), + ...(unique !== undefined && { unique }), + ...(not_null !== undefined && { not_null }), + ...(increment !== undefined && { increment }), + ...(dbdefault !== undefined && { dbdefault }), + ...(note && { note }), + _hasBracketSettings: hasBracketSettings, + }); + + return new Report(result, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/table/utils.ts b/packages/dbml-parse/src/core/global_modules/table/utils.ts new file mode 100644 index 000000000..83c667c34 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/table/utils.ts @@ -0,0 +1,150 @@ +import type Compiler from '@/compiler'; +import type { SyntaxNode } from '@/core/types/nodes'; +import Report from '@/core/types/report'; +import { DEFAULT_SCHEMA_NAME, KEYWORDS_OF_DEFAULT_SETTING, UNHANDLED } from '@/constants'; +import { lookupMember, nodeRefereeOfLeftExpression } from '../utils'; +import { type NodeSymbol, SchemaSymbol, SymbolKind } from '@/core/types/symbols'; +import { isAccessExpression, isExpressionAVariableNode } from '@/core/utils/expression'; +import { InfixExpressionNode } from '@/core/types/nodes'; +import { extractVarNameFromPrimaryVariable } from '@/core/syntax/utils'; + +// Check if a name matches any table's alias in the given scope +function findTableByAlias (compiler: Compiler, parentSymbol: NodeSymbol, aliasName: string): NodeSymbol | undefined { + const members = compiler.symbolMembers(parentSymbol); + if (members.hasValue(UNHANDLED)) return undefined; + return members.getValue().find((m: NodeSymbol) => { + if (!m.isKind(SymbolKind.Table) || !m.declaration) return false; + const aliasResult = compiler.alias(m.declaration); + if (aliasResult.hasValue(UNHANDLED)) return false; + return aliasResult.getValue() === aliasName; + }); +} + +// Look up a member in the default (public) schema, falling back to direct program search +function lookupInDefaultSchema (compiler: Compiler, globalSymbol: NodeSymbol, name: string, opts: { kinds?: SymbolKind[]; ignoreNotFound?: boolean; errorNode?: SyntaxNode }): Report { + const members = compiler.symbolMembers(globalSymbol); + if (!members.hasValue(UNHANDLED)) { + const publicSchema = members.getValue().find((m: NodeSymbol) => m instanceof SchemaSymbol && m.name === DEFAULT_SCHEMA_NAME); + if (publicSchema) { + return lookupMember(compiler, publicSchema, name, opts); + } + } + return lookupMember(compiler, globalSymbol, name, opts); +} + +// Column type: enum or schema.enum or schema.schema.enum +export function nodeRefereeOfEnumType (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + // Standalone: try as enum in default schema, ignore if not found (could be a raw type like varchar) + if (!isAccessExpression(node.parent)) { + return lookupInDefaultSchema(compiler, globalSymbol, name, { kinds: [SymbolKind.Enum], ignoreNotFound: true, errorNode: node }); + } + + // Right side of access - resolve via left sibling + const left = nodeRefereeOfLeftExpression(compiler, node); + if (left) { + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Enum, SymbolKind.Schema], errorNode: node }); + } + return new Report(undefined); + } + + // Left side of access - look up as Schema in program scope + const parent = node.parent as InfixExpressionNode; + if (parent.leftExpression === node) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Schema], ignoreNotFound: true, errorNode: node }); + } + + return new Report(undefined); +} + +// Inline ref: table.column or schema.table.column +// Always report errors, never ignore not found +export function nodeRefereeOfInlineRef (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + // Standalone variable in inline ref - ambiguous (column name without table), ignore not found + if (!isAccessExpression(node.parent)) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Column], ignoreNotFound: true, errorNode: node }); + } + + // Right side of access expression - resolve via left sibling + const left = nodeRefereeOfLeftExpression(compiler, node); + if (left) { + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Table, SymbolKind.Schema], errorNode: node }); + } + if (left.isKind(SymbolKind.Table)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Column], errorNode: node }); + } + return new Report(undefined); + } + + // Left side of access expression - look up as Table or Schema in program scope + const parent = node.parent as InfixExpressionNode; + if (parent.leftExpression === node) { + // If our parent is also a left side of another access, this is a schema + if (isAccessExpression(parent.parent) && (parent.parent as InfixExpressionNode).leftExpression === parent) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Schema], errorNode: node }); + } + // First try by table name, then by alias + const tableResult = lookupInDefaultSchema(compiler, globalSymbol, name, { kinds: [SymbolKind.Table], ignoreNotFound: true, errorNode: node }); + if (tableResult.getValue()) return tableResult; + // Try alias lookup in default schema + const members = compiler.symbolMembers(globalSymbol); + if (!members.hasValue(UNHANDLED)) { + const publicSchema = members.getValue().find((m: NodeSymbol) => m instanceof SchemaSymbol && m.name === DEFAULT_SCHEMA_NAME); + if (publicSchema) { + const byAlias = findTableByAlias(compiler, publicSchema, name); + if (byAlias) return new Report(byAlias); + } + } + const byAlias = findTableByAlias(compiler, globalSymbol, name); + if (byAlias) return new Report(byAlias); + // Not found by name or alias - report error + return lookupInDefaultSchema(compiler, globalSymbol, name, { kinds: [SymbolKind.Table], errorNode: node }); + } + + return new Report(undefined); +} + +// Default value: enum.field or schema.enum.field +export function nodeRefereeOfEnumDefault (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + // Standalone: ignore default keywords (true/false/null), everything else is an enum lookup + if (!isAccessExpression(node.parent)) { + if (KEYWORDS_OF_DEFAULT_SETTING.includes(name.toLowerCase())) { + return new Report(undefined); + } + return lookupInDefaultSchema(compiler, globalSymbol, name, { kinds: [SymbolKind.Enum], ignoreNotFound: true, errorNode: node }); + } + + // Right side of access - resolve via left sibling + const left = nodeRefereeOfLeftExpression(compiler, node); + if (left) { + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Enum, SymbolKind.Schema], errorNode: node }); + } + if (left.isKind(SymbolKind.Enum)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.EnumField], errorNode: node }); + } + return new Report(undefined); + } + + // Left side of access - look up as Enum in program scope (report errors since it's clearly an enum access) + const parent = node.parent as InfixExpressionNode; + if (parent.leftExpression === node) { + // If parent is also left of another access, this is a schema + if (isAccessExpression(parent.parent) && (parent.parent as InfixExpressionNode).leftExpression === parent) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Schema], errorNode: node }); + } + return lookupInDefaultSchema(compiler, globalSymbol, name, { kinds: [SymbolKind.Enum], errorNode: node }); + } + + return new Report(undefined); +} diff --git a/packages/dbml-parse/src/core/global_modules/tableGroup/bind.ts b/packages/dbml-parse/src/core/global_modules/tableGroup/bind.ts new file mode 100644 index 000000000..e6fc01e29 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/tableGroup/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindTableGroup (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.TableGroup) && !isElementFieldNode(node, ElementKind.TableGroup)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/tableGroup/index.ts b/packages/dbml-parse/src/core/global_modules/tableGroup/index.ts new file mode 100644 index 000000000..8293a9490 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/tableGroup/index.ts @@ -0,0 +1,117 @@ +import { isElementNode, isExpressionAVariableNode, isAccessExpression, isElementFieldNode, isInsideElementBody, isInsideSettingList } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { DEFAULT_SCHEMA_NAME, PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import type { SchemaElement } from '@/core/types/schemaJson'; +import { getNodeMemberSymbols, getSymbolDirectMembers, lookupMember, nodeRefereeOfLeftExpression } from '../utils'; +import { extractVarNameFromPrimaryVariable } from '@/core/syntax/utils'; +import { interpretTableGroup, interpretTableGroupField } from './interpret'; +import { bindTableGroup } from './bind'; + +export const tableGroupModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TableGroup)) { + return new Report(new NodeSymbol({ + kind: SymbolKind.TableGroup, + declaration: node, + })); + } + if (isElementFieldNode(node, ElementKind.TableGroup)) { + return new Report(new NodeSymbol({ kind: SymbolKind.TableGroupField, declaration: node })); + } + return Report.create(PASS_THROUGH); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (symbol.isKind(SymbolKind.TableGroup)) { + return getSymbolDirectMembers(compiler, symbol); + } + if (symbol.isKind(SymbolKind.TableGroupField)) { + return new Report([]); + } + return Report.create(PASS_THROUGH); + }, + + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TableGroup)) { + return getNodeMemberSymbols(compiler, node); + } + if (isElementFieldNode(node, ElementKind.TableGroup)) { + return new Report([]); + } + return Report.create(PASS_THROUGH); + }, + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isExpressionAVariableNode(node)) return Report.create(PASS_THROUGH); + if (!isInsideElementBody(node, ElementKind.TableGroup)) return Report.create(PASS_THROUGH); + // Skip variables inside setting lists + if (node.parent && isInsideSettingList(node)) return Report.create(PASS_THROUGH); + + const programNode = compiler.parseFile().getValue().ast; + const globalSymbol = compiler.nodeSymbol(programNode).getValue(); + if (globalSymbol === UNHANDLED) return Report.create(undefined); + + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + // Standalone: lookup as Table in default schema, or Schema + // Try by name first, then by alias (ignoreNotFound for each attempt), + // and report an error only if all attempts fail. + if (!isAccessExpression(node.parent)) { + // Try in public schema first for tables + const members = compiler.symbolMembers(globalSymbol); + if (!members.hasValue(UNHANDLED)) { + const publicSchema = members.getValue().find((m: any) => m.name === DEFAULT_SCHEMA_NAME && m.isKind(SymbolKind.Schema)); + if (publicSchema) { + const tableResult = lookupMember(compiler, publicSchema, name, { kinds: [SymbolKind.Table], ignoreNotFound: true, errorNode: node }); + if (tableResult.getValue()) return tableResult; + // Try by alias: check if any table in the schema has this alias + const schemaMembers = compiler.symbolMembers(publicSchema); + if (!schemaMembers.hasValue(UNHANDLED)) { + for (const m of schemaMembers.getValue()) { + if (!m.isKind(SymbolKind.Table) || !m.declaration) continue; + const aliasResult = compiler.alias(m.declaration); + if (!aliasResult.hasValue(UNHANDLED) && aliasResult.getValue() === name) { + return new Report(m); + } + } + } + } + } + // Try global scope for schemas + const globalResult = lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Table, SymbolKind.Schema], ignoreNotFound: true, errorNode: node }); + if (globalResult.getValue()) return globalResult; + // Nothing found - report error + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Table], ignoreNotFound: false, errorNode: node }); + } + + // Right side of access: resolve via left sibling + const left = nodeRefereeOfLeftExpression(compiler, node); + if (left) { + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Table, SymbolKind.Schema] }); + } + return new Report(undefined); + } + + // Left side of access: look up as Schema + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Schema] }); + }, + + bind: bindTableGroup, + + interpret (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TableGroup)) { + return interpretTableGroup(compiler, node); + } + if (isElementFieldNode(node, ElementKind.TableGroup)) { + return interpretTableGroupField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/global_modules/tableGroup/interpret.ts b/packages/dbml-parse/src/core/global_modules/tableGroup/interpret.ts new file mode 100644 index 000000000..dcb5671cd --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/tableGroup/interpret.ts @@ -0,0 +1,135 @@ +import { isElementNode, isElementFieldNode, getBody } from '@/core/utils/expression'; +import { ElementKind } from '@/core/types/keywords'; +import { BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, PrimaryExpressionNode, LiteralNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { extractQuotedStringToken } from '@/core/syntax/utils'; +import { getTokenPosition, normalizeNoteContent } from '../utils'; +import { TableGroupField, TableGroup, schemaFrom } from '@/core/types/schemaJson'; +import type { TokenPosition } from '@/core/types/schemaJson'; +import type { CompileError } from '@/core/types/errors'; +import { SyntaxTokenKind } from '@/core/types/tokens'; + +function extractColorValue (node: unknown): string | undefined { + if (node instanceof PrimaryExpressionNode && node.expression instanceof LiteralNode && node.expression.literal?.kind === SyntaxTokenKind.COLOR_LITERAL) { + return node.expression.literal.value; + } + return undefined; +} + +export function interpretTableGroup (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.TableGroup)) return Report.create(PASS_THROUGH); + + // Derive schema-qualified group name + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const lastSegment = name?.at(-1) ?? null; + const schemaSegments = name?.slice(0, -1) ?? []; + + const token = getTokenPosition(node); + const errors: CompileError[] = []; + + // Resolve each member's fully-qualified table reference + const symbolResult = compiler.nodeSymbol(node); + const membersResult = !symbolResult.hasValue(UNHANDLED) ? compiler.symbolMembers(symbolResult.getValue()) : undefined; + const members = (membersResult && !membersResult.hasValue(UNHANDLED)) ? membersResult.getValue() : []; + + // Delegate interpretation of each field to tableGroupFieldModule + const tables: TableGroupField[] = []; + for (const member of members) { + if (!member.declaration) continue; + const fieldResult = compiler.interpret(member.declaration); + if (!fieldResult.hasValue(UNHANDLED)) { + const field = fieldResult.getValue(); + if (field instanceof TableGroupField) tables.push(field); + errors.push(...fieldResult.getErrors()); + } + } + + // Detect body-level notes and note: fields + let bodyNote: { value: string; token: TokenPosition } | undefined; + const bodyFields = getBody(node); + for (const field of bodyFields) { + // Note sub-element: `Note { ... }` or `Note: 'string'` + if (field instanceof ElementDeclarationNode && isElementNode(field, ElementKind.Note)) { + let noteContent: string | undefined; + if (field.bodyColon && field.body instanceof FunctionApplicationNode) { + noteContent = extractQuotedStringToken(field.body.callee); + } else if (!field.bodyColon && field.body instanceof BlockExpressionNode) { + const firstField = field.body.body[0]; + if (firstField instanceof FunctionApplicationNode) { + noteContent = extractQuotedStringToken(firstField.callee); + } + } + if (noteContent !== undefined) { + bodyNote = { value: normalizeNoteContent(noteContent), token: getTokenPosition(field) }; + } + continue; + } + // note: 'string' field form (e.g. `note: 'simple note'`) + if (field instanceof ElementDeclarationNode && field.bodyColon && field.body instanceof FunctionApplicationNode) { + const fieldName = field.type?.value?.toLowerCase(); + if (fieldName === 'note') { + const noteContent = extractQuotedStringToken(field.body.callee); + if (noteContent !== undefined) { + bodyNote = { value: normalizeNoteContent(noteContent), token: getTokenPosition(field) }; + } + } + } + } + + // Extract settings + const settingsResult = compiler.settings(node); + errors.push(...settingsResult.getErrors()); + + let color: string | undefined; + let note: { value: string; token: TokenPosition } | undefined; + + if (!settingsResult.hasValue(UNHANDLED)) { + const settings = settingsResult.getValue(); + if (settings.get('color')?.[0]?.value) { + color = extractQuotedStringToken(settings.get('color')![0].value) ?? extractColorValue(settings.get('color')![0].value); + } + + if (settings.get('note')?.[0]?.value) { + const noteValue = extractQuotedStringToken(settings.get('note')![0].value); + if (noteValue !== undefined) { + note = { + value: normalizeNoteContent(noteValue), + token: getTokenPosition(settings.get('note')![0]), + }; + } + } + } + + // Body-level note overrides settings-level note + if (bodyNote) { + note = bodyNote; + } + + return new Report(schemaFrom(TableGroup, { + name: lastSegment, + schemaName: schemaSegments.join('.') || null, + tables, + token, + ...(color && { color }), + ...(note && { note }), + }), errors); +} + +export function interpretTableGroupField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.TableGroup)) return Report.create(PASS_THROUGH); + + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const fieldName = name?.at(-1) ?? ''; + const fieldSchema = name && name.length > 1 ? name.slice(0, -1).join('.') : ''; + + return new Report(schemaFrom(TableGroupField, { + name: fieldName, + schemaName: fieldSchema, + token: getTokenPosition(node), + })); +} diff --git a/packages/dbml-parse/src/core/global_modules/tableGroup/utils.ts b/packages/dbml-parse/src/core/global_modules/tableGroup/utils.ts new file mode 100644 index 000000000..70ca3651a --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/tableGroup/utils.ts @@ -0,0 +1 @@ +// Module-specific utilities diff --git a/packages/dbml-parse/src/core/global_modules/tablePartial/bind.ts b/packages/dbml-parse/src/core/global_modules/tablePartial/bind.ts new file mode 100644 index 000000000..1e1b2db2f --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/tablePartial/bind.ts @@ -0,0 +1,20 @@ +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { bindNode, bindDeep } from '../utils/bind'; + +export function bindTablePartial (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.TablePartial) && !isElementFieldNode(node, ElementKind.TablePartial)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + errors.push(...bindNode(compiler, node)); + errors.push(...bindDeep(compiler, node)); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/tablePartial/index.ts b/packages/dbml-parse/src/core/global_modules/tablePartial/index.ts new file mode 100644 index 000000000..f619e782e --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/tablePartial/index.ts @@ -0,0 +1,83 @@ +import { isElementNode, isExpressionAVariableNode, isAccessExpression, isElementFieldNode, isInsideElementBody, isWithinNthArgOfField, isInsideSettingValue } from '@/core/utils/expression'; +import { ElementKind, SettingName } from '@/core/types/keywords'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { type GlobalModule as Module } from '../types'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import type { SchemaElement } from '@/core/types/schemaJson'; +import { getNodeMemberSymbols, getSymbolDirectMembers } from '../utils'; +import { nodeRefereeOfEnumType, nodeRefereeOfInlineRef, nodeRefereeOfEnumDefault } from './utils'; +import { interpretTablePartial, interpretTablePartialField } from './interpret'; +import { bindTablePartial } from './bind'; + +export const tablePartialModule: Module = { + nodeSymbol (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TablePartial)) { + return new Report(new NodeSymbol({ + kind: SymbolKind.TablePartial, + declaration: node, + })); + } + if (isElementFieldNode(node, ElementKind.TablePartial)) { + return new Report(new NodeSymbol({ kind: SymbolKind.Column, declaration: node })); + } + return Report.create(PASS_THROUGH); + }, + + symbolMembers (compiler: Compiler, symbol: NodeSymbol): Report | Report { + if (symbol.isKind(SymbolKind.TablePartial)) { + return getSymbolDirectMembers(compiler, symbol); + } + if (symbol.isKind(SymbolKind.TablePartialField)) { + return new Report([]); + } + return Report.create(PASS_THROUGH); + }, + + + nodeReferee (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isExpressionAVariableNode(node) && !isAccessExpression(node)) return Report.create(PASS_THROUGH); + if (!isInsideElementBody(node, ElementKind.TablePartial)) return Report.create(PASS_THROUGH); + + const programNode = compiler.parseFile().getValue().ast; + const globalSymbol = compiler.nodeSymbol(programNode).getValue(); + if (globalSymbol === UNHANDLED) return Report.create(undefined); + + // Case 1: Column's enum type + if (isWithinNthArgOfField(node, 1)) { + return nodeRefereeOfEnumType(compiler, globalSymbol, node); + } + + // Case 2: Column's inline ref + if (isInsideSettingValue(node, SettingName.Ref)) { + return nodeRefereeOfInlineRef(compiler, globalSymbol, node); + } + + // Case 3: Column's default value being an enum value + return nodeRefereeOfEnumDefault(compiler, globalSymbol, node); + }, + + nestedSymbols (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TablePartial)) { + return getNodeMemberSymbols(compiler, node); + } + if (isElementFieldNode(node, ElementKind.TablePartial)) { + return new Report([]); + } + return Report.create(PASS_THROUGH); + }, + + bind: bindTablePartial, + + interpret (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TablePartial)) { + return interpretTablePartial(compiler, node); + } + if (isElementFieldNode(node, ElementKind.TablePartial)) { + return interpretTablePartialField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/global_modules/tablePartial/interpret.ts b/packages/dbml-parse/src/core/global_modules/tablePartial/interpret.ts new file mode 100644 index 000000000..fbd898ffd --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/tablePartial/interpret.ts @@ -0,0 +1,318 @@ +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { ArrayNode, BlockExpressionNode, CallExpressionNode, ElementDeclarationNode, FunctionApplicationNode, FunctionExpressionNode, ListExpressionNode, PrefixExpressionNode } from '@/core/types/nodes'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { PASS_THROUGH, type PassThrough, UNHANDLED } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler/index'; +import { destructureComplexVariable, extractQuotedStringToken, isBinaryRelationship, destructureComplexVariableTuple } from '@/core/syntax/utils'; +import { getTokenPosition, normalizeNoteContent } from '../utils'; +import { Column, Check, Index, InlineRef, TablePartial, schemaFrom } from '@/core/types/schemaJson'; +import type { TokenPosition, ColumnType } from '@/core/types/schemaJson'; +import { isExpressionAQuotedString, isExpressionAVariableNode, isExpressionASignedNumberExpression, getNumberTextFromExpression, parseNumber, isRelationshipOp, isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { PrimaryExpressionNode, LiteralNode } from '@/core/types/nodes'; +import { SyntaxTokenKind } from '@/core/types/tokens'; +import type { CompileError } from '@/core/types/errors'; + +function extractColorValue (node: unknown): string | undefined { + if (node instanceof PrimaryExpressionNode && node.expression instanceof LiteralNode && node.expression.literal?.kind === SyntaxTokenKind.COLOR_LITERAL) { + return node.expression.literal.value; + } + return undefined; +} + +export function interpretTablePartial (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.TablePartial)) return Report.create(PASS_THROUGH); + + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const lastSegment = name?.at(-1) ?? ''; + + const token = getTokenPosition(node); + const errors: CompileError[] = []; + + // Interpret column members, same as a regular table but without schema qualification + const symbolResult = compiler.nodeSymbol(node); + const membersResult = !symbolResult.hasValue(UNHANDLED) ? compiler.symbolMembers(symbolResult.getValue()) : undefined; + const members = (membersResult && !membersResult.hasValue(UNHANDLED)) ? membersResult.getValue() : []; + + const fields: Column[] = []; + for (const member of members) { + if (!member.declaration) continue; + const fieldResult = compiler.interpret(member.declaration); + if (!fieldResult.hasValue(UNHANDLED)) { + const field = fieldResult.getValue(); + if (field instanceof Column) fields.push(field); + errors.push(...fieldResult.getErrors()); + } + } + + // Extract settings + const settingsResult = compiler.settings(node); + errors.push(...settingsResult.getErrors()); + + let headerColor: string | undefined; + let note: { value: string; token: TokenPosition } | undefined; + + if (!settingsResult.hasValue(UNHANDLED)) { + const settings = settingsResult.getValue(); + if (settings.get('headercolor')?.[0]?.value) { + const colorNode = settings.get('headercolor')![0].value; + headerColor = extractColorValue(colorNode) ?? extractQuotedStringToken(colorNode); + } + + if (settings.get('note')?.[0]?.value) { + const noteValue = extractQuotedStringToken(settings.get('note')![0].value); + if (noteValue !== undefined) { + note = { + value: normalizeNoteContent(noteValue), + token: getTokenPosition(settings.get('note')![0]), + }; + } + } + } + + // Process sub-elements: checks, indexes, body-level notes + const checks: Check[] = []; + const indexes: Index[] = []; + let bodyNote: { value: string; token: TokenPosition } | undefined; + const body = node.body; + if (body instanceof BlockExpressionNode) { + const subs = body.body.filter((e): e is ElementDeclarationNode => e instanceof ElementDeclarationNode); + for (const sub of subs) { + // Detect body-level Note sub-elements + if (isElementNode(sub, ElementKind.Note)) { + let noteContent: string | undefined; + if (sub.bodyColon && sub.body instanceof FunctionApplicationNode) { + noteContent = extractQuotedStringToken(sub.body.callee); + } else if (!sub.bodyColon && sub.body instanceof BlockExpressionNode) { + const firstField = sub.body.body[0]; + if (firstField instanceof FunctionApplicationNode) { + noteContent = extractQuotedStringToken(firstField.callee); + } + } + if (noteContent !== undefined) { + bodyNote = { value: normalizeNoteContent(noteContent), token: getTokenPosition(sub) }; + continue; + } + } + + const result = compiler.interpret(sub); + if (result.hasValue(UNHANDLED)) continue; + const value = result.getValue(); + errors.push(...result.getErrors()); + if (Array.isArray(value)) { + for (const v of value) { + if (v instanceof Check) checks.push(v); + else if (v instanceof Index) indexes.push(v); + } + } + } + } + + // Body-level note overrides settings-level note + if (bodyNote) { + note = bodyNote; + } + + return new Report(schemaFrom(TablePartial, { + name: lastSegment, + fields, + indexes, + checks, + token, + ...(headerColor && { headerColor }), + ...(note && { note }), + }), errors); +} + +export function interpretTablePartialField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.TablePartial)) return Report.create(PASS_THROUGH); + + const fullnameResult = compiler.fullname(node); + const name = !fullnameResult.hasValue(UNHANDLED) ? fullnameResult.getValue() : undefined; + const fieldName = name?.at(-1) ?? ''; + + const token = getTokenPosition(node); + const errors: CompileError[] = []; + + // Parse column type using the same logic as interpretTableField + let rawTypeNode: SyntaxNode | undefined = node.args[0]; + let columnType: ColumnType = { schemaName: null, type_name: '', args: null }; + + if (rawTypeNode) { + let typeSuffix = ''; + let typeArgs: string | null = null; + + if (rawTypeNode instanceof CallExpressionNode && rawTypeNode.argumentList) { + typeArgs = rawTypeNode.argumentList.elementList.map((e) => { + if (isExpressionASignedNumberExpression(e)) return getNumberTextFromExpression(e); + if (isExpressionAQuotedString(e)) return extractQuotedStringToken(e) ?? ''; + if (isExpressionAVariableNode(e)) return e.expression.variable.value; + return ''; + }).join(','); + typeSuffix = `(${typeArgs})`; + rawTypeNode = rawTypeNode.callee; + } + + while (rawTypeNode instanceof CallExpressionNode || rawTypeNode instanceof ArrayNode) { + if (rawTypeNode instanceof CallExpressionNode) { + const args = rawTypeNode.argumentList?.elementList.map((e) => { + if (isExpressionASignedNumberExpression(e)) return getNumberTextFromExpression(e); + if (isExpressionAQuotedString(e)) return extractQuotedStringToken(e) ?? ''; + if (isExpressionAVariableNode(e)) return e.expression.variable.value; + return ''; + }).join(',') ?? ''; + typeSuffix = `(${args})${typeSuffix}`; + rawTypeNode = rawTypeNode.callee; + } else { + const indexer = `[${rawTypeNode.indexer?.elementList.map((e: any) => e?.name?.expression?.literal?.value ?? '').join(',') ?? ''}]`; + typeSuffix = `${indexer}${typeSuffix}`; + rawTypeNode = rawTypeNode.array; + } + } + + const typeFragments = rawTypeNode ? destructureComplexVariable(rawTypeNode) : undefined; + if (typeFragments && typeFragments.length > 0) { + const typeName = typeFragments.at(-1) ?? ''; + const typeSchema = typeFragments.length > 1 ? typeFragments.slice(0, -1).join('.') : null; + columnType = { + schemaName: typeSchema, + type_name: `${typeName}${typeSuffix}`, + args: typeArgs, + }; + } + } + + // Extract column-level settings (pk, unique, not null, default, note, inline refs) + const settingsResult = compiler.settings(node); + errors.push(...settingsResult.getErrors()); + + let pk: boolean | undefined; + let unique: boolean | undefined; + let not_null: boolean | undefined; + let increment: boolean | undefined; + let dbdefault: Column['dbdefault'] | undefined; + let note: { value: string; token: TokenPosition } | undefined; + const inline_refs: InlineRef[] = []; + const columnChecks: Check[] = []; + + if (!settingsResult.hasValue(UNHANDLED)) { + const settings = settingsResult.getValue(); + + if (settings.get('pk') || settings.get('primary key')) { + pk = true; + } + if (settings.get('unique')) { + unique = true; + } + if (settings.get('not null')) { + not_null = true; + } + if (settings.get('null')) { + not_null = false; + } + if (settings.get('increment')) { + increment = true; + } + if (settings.get('default')?.[0]?.value) { + const defaultNode = settings.get('default')![0].value; + if (isExpressionAQuotedString(defaultNode)) { + dbdefault = { type: 'string', value: extractQuotedStringToken(defaultNode) ?? '' }; + } else if (isExpressionASignedNumberExpression(defaultNode)) { + dbdefault = { type: 'number', value: parseNumber(defaultNode) }; + } else if (defaultNode instanceof FunctionExpressionNode) { + dbdefault = { type: 'expression', value: defaultNode.value?.value ?? '' }; + } else if (isExpressionAVariableNode(defaultNode)) { + const val = defaultNode.expression.variable.value.toLowerCase(); + dbdefault = { type: 'boolean', value: val }; + } else { + const fragments = destructureComplexVariable(defaultNode); + if (fragments && fragments.length > 0) { + dbdefault = { type: 'string', value: fragments.at(-1) ?? '' }; + } + } + } + if (settings.get('note')?.[0]?.value) { + const noteValue = extractQuotedStringToken(settings.get('note')![0].value); + if (noteValue !== undefined) { + note = { + value: normalizeNoteContent(noteValue), + token: getTokenPosition(settings.get('note')![0]), + }; + } + } + + // Parse column-level check constraints + if (settings.get(SettingName.Check)) { + for (const checkAttr of settings.get(SettingName.Check)!) { + if (!checkAttr.value) continue; + if (checkAttr.value instanceof FunctionExpressionNode) { + const expression = checkAttr.value.value?.value ?? ''; + columnChecks.push(schemaFrom(Check, { + expression, + token: getTokenPosition(checkAttr), + })); + } + } + } + + // Parse inline ref declarations (e.g. `ref: > other_table.id`) into endpoint objects + if (settings.get('ref')) { + for (const refAttr of settings.get('ref')!) { + if (!refAttr.value) continue; + if (isBinaryRelationship(refAttr.value)) { + const op = refAttr.value.op?.value; + const rightTuple = destructureComplexVariableTuple(refAttr.value.rightExpression); + if (rightTuple && op && isRelationshipOp(op)) { + const vars = rightTuple.variables; + const tableName = vars.map((v) => v.expression.variable?.value ?? '').at(-1) ?? ''; + const schemaName = vars.length > 1 ? vars.slice(0, -1).map((v) => v.expression.variable?.value ?? '').join('.') : null; + const fieldNames = rightTuple.tupleElements.length > 0 + ? rightTuple.tupleElements.map((e) => e.expression.variable?.value ?? '') + : []; + inline_refs.push(new InlineRef(getTokenPosition(refAttr), schemaName, tableName, fieldNames, op)); + } + } else if (refAttr.value instanceof PrefixExpressionNode && isRelationshipOp(refAttr.value.op?.value)) { + const op = refAttr.value.op!.value as '>' | '<' | '-' | '<>'; + const targetTuple = destructureComplexVariableTuple(refAttr.value.expression); + if (targetTuple) { + const vars = targetTuple.variables.map((v) => v.expression.variable?.value ?? ''); + let tableName: string; + let schemaName: string | null; + let fieldNames: string[]; + + if (targetTuple.tupleElements.length > 0) { + tableName = vars.at(-1) ?? ''; + schemaName = vars.length > 1 ? vars.slice(0, -1).join('.') : null; + fieldNames = targetTuple.tupleElements.map((e) => e.expression.variable?.value ?? ''); + } else { + fieldNames = vars.length > 0 ? [vars.at(-1)!] : []; + tableName = vars.length > 1 ? vars.at(-2)! : ''; + schemaName = vars.length > 2 ? vars.slice(0, -2).join('.') : null; + } + inline_refs.push(new InlineRef(getTokenPosition(refAttr), schemaName, tableName, fieldNames, op)); + } + } + } + } + } + + const hasBracketSettings = node.args.some((a) => a instanceof ListExpressionNode); + + const result = schemaFrom(Column, { + name: fieldName, + type: columnType, + inline_refs, + checks: columnChecks, + token, + ...(pk !== undefined && { pk }), + ...(unique !== undefined && { unique }), + ...(not_null !== undefined && { not_null }), + ...(increment !== undefined && { increment }), + ...(dbdefault !== undefined && { dbdefault }), + ...(note && { note }), + _hasBracketSettings: hasBracketSettings, + }); + + return new Report(result, errors); +} diff --git a/packages/dbml-parse/src/core/global_modules/tablePartial/utils.ts b/packages/dbml-parse/src/core/global_modules/tablePartial/utils.ts new file mode 100644 index 000000000..1a8e743c7 --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/tablePartial/utils.ts @@ -0,0 +1,78 @@ +import type Compiler from '@/compiler'; +import type { SyntaxNode } from '@/core/types/nodes'; +import Report from '@/core/types/report'; +import { lookupMember, nodeRefereeOfLeftExpression, lookupInDefaultSchema } from '../utils'; +import { type NodeSymbol, SymbolKind } from '@/core/types/symbols'; +import { isAccessExpression, isExpressionAVariableNode } from '@/core/utils/expression'; +import { extractVarNameFromPrimaryVariable } from '@/core/syntax/utils'; + +// Column type: enum or schema.enum or schema.schema.enum +export function nodeRefereeOfEnumType (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + // Standalone: try as enum, ignore if not found (could be a raw type like varchar) + if (!isAccessExpression(node.parent)) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Enum], ignoreNotFound: true }); + } + + // In access expression: must resolve, report errors + const left = nodeRefereeOfLeftExpression(compiler, node); + if (!left) return new Report(undefined); + + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Enum, SymbolKind.Schema] }); + } + + return new Report(undefined); +} + +// Inline ref: column or schema.table.column +// Standalone variables are ignored (could be partial-local column references) +export function nodeRefereeOfInlineRef (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + if (!isAccessExpression(node.parent)) { + return lookupMember(compiler, globalSymbol, name, { kinds: [SymbolKind.Column], ignoreNotFound: true, errorNode: node }); + } + + // Right side of access: resolve using the left sibling's referee + const left = nodeRefereeOfLeftExpression(compiler, node); + if (left) { + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Table, SymbolKind.Schema] }); + } + if (left.isKind(SymbolKind.Table)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Column] }); + } + return new Report(undefined); + } + + // Left side of access: look up as Table or Schema in default schema + return lookupInDefaultSchema(compiler, globalSymbol, name, { kinds: [SymbolKind.Table, SymbolKind.Schema], ignoreNotFound: true, errorNode: node }); +} + +// Default value: enum.field or schema.enum.field +export function nodeRefereeOfEnumDefault (compiler: Compiler, globalSymbol: NodeSymbol, node: SyntaxNode): Report { + if (!isExpressionAVariableNode(node)) return new Report(undefined); + const name = extractVarNameFromPrimaryVariable(node) ?? ''; + + // Standalone: ignore (could be a literal like null/true/false) + if (!isAccessExpression(node.parent)) { + return new Report(undefined); + } + + // In access expression: must resolve, report errors + const left = nodeRefereeOfLeftExpression(compiler, node); + if (!left) return new Report(undefined); + + if (left.isKind(SymbolKind.Schema)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.Enum, SymbolKind.Schema] }); + } + if (left.isKind(SymbolKind.Enum)) { + return lookupMember(compiler, left, name, { kinds: [SymbolKind.EnumField] }); + } + + return new Report(undefined); +} diff --git a/packages/dbml-parse/src/core/global_modules/types.ts b/packages/dbml-parse/src/core/global_modules/types.ts new file mode 100644 index 000000000..bf9d1f61b --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/types.ts @@ -0,0 +1,25 @@ +import type { PassThrough } from '@/constants'; +import type Compiler from '@/compiler/index'; +import type { SyntaxNode } from '@/core/types/nodes'; +import type { NodeSymbol } from '../types/symbols'; +import type Report from '../types/report'; +import type { SchemaElement } from '../types/schemaJson'; +import type { Module } from '../types/module'; + +// Modules decouple element-specific logic from the compiler: each module handles one DBML element kind +// (table, enum, ref, etc.) and the compiler dispatches to them via a chain-of-responsibility pattern. +// All methods are optional, missing methods are treated as returning PASS_THROUGH. +export interface GlobalModule extends Module { + // Produce the unique symbol identity for this AST node + nodeSymbol? (compiler: Compiler, node: SyntaxNode): Report | Report; + // List the direct child symbols owned by this symbol (e.g. columns of a table) + symbolMembers? (compiler: Compiler, symbol: NodeSymbol): Report | Report; + // List all symbols syntactically nested under this node (recursive) + nestedSymbols? (compiler: Compiler, node: SyntaxNode): Report | Report; + // Resolve the symbol that this reference node points to + nodeReferee? (compiler: Compiler, node: SyntaxNode): Report | Report; + // Resolve cross-references for this node (e.g. link ref endpoints to their target columns) + bind? (compiler: Compiler, node: SyntaxNode): Report | Report; + // Convert this AST node into its schema JSON representation + interpret? (compiler: Compiler, node: SyntaxNode): Report | Report; +} diff --git a/packages/dbml-parse/src/core/global_modules/utils/bind.ts b/packages/dbml-parse/src/core/global_modules/utils/bind.ts new file mode 100644 index 000000000..5e39000fd --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/utils/bind.ts @@ -0,0 +1,59 @@ +import type Compiler from '@/compiler/index'; +import { SyntaxNode } from '@/core/types/nodes'; +import { UNHANDLED } from '@/constants'; +import { isExpressionAVariableNode } from '@/core/utils/expression'; +import type { CompileError } from '@/core/types/errors'; +import { getMemberChain } from '@/core/syntax/parser/utils'; + +// Trigger nodeSymbol + symbolMembers on a node, always collect errors. +export function bindNode (compiler: Compiler, node: SyntaxNode): CompileError[] { + const errors: CompileError[] = []; + const symResult = compiler.nodeSymbol(node); + errors.push(...symResult.getErrors()); + // Trigger symbolMembers to detect duplicates + if (!symResult.hasValue(UNHANDLED)) { + errors.push(...compiler.symbolMembers(symResult.getValue()).getErrors()); + } + return errors; +} + +// Trigger validate on a node, collecting errors +export function validateNode (compiler: Compiler, node: SyntaxNode): CompileError[] { + const errors: CompileError[] = []; + const validateResult = compiler.validate(node); + if (!validateResult.hasValue(UNHANDLED)) { + errors.push(...validateResult.getErrors()); + } + return errors; +} + +// Deep walk: trigger compiler.bind on element/field nodes, +// trigger compiler.nodeReferee on variable nodes, +// always collect errors regardless of UNHANDLED +export function bindDeep (compiler: Compiler, node: SyntaxNode): CompileError[] { + const errors: CompileError[] = []; + + const walk = (n: SyntaxNode): void => { + if (isExpressionAVariableNode(n)) { + errors.push(...compiler.nodeReferee(n).getErrors()); + return; + } + + // Try to bind this node. If a module handled it, its bind + // recursively walks its own children, so we stop here. + const result = compiler.bind(n); + errors.push(...result.getErrors()); + if (!result.hasValue(UNHANDLED)) return; // handled, children already bound + + // UNHANDLED: no module claimed this node, walk children manually + for (const child of getMemberChain(n)) { + if (child instanceof SyntaxNode) walk(child); + } + }; + + for (const child of getMemberChain(node)) { + if (child instanceof SyntaxNode) walk(child); + } + + return errors; +} diff --git a/packages/dbml-parse/src/core/global_modules/utils/index.ts b/packages/dbml-parse/src/core/global_modules/utils/index.ts new file mode 100644 index 000000000..fc92ea58c --- /dev/null +++ b/packages/dbml-parse/src/core/global_modules/utils/index.ts @@ -0,0 +1,266 @@ +import type Compiler from '@/compiler/index'; +import { + ElementDeclarationNode, + InfixExpressionNode, + PostfixExpressionNode, + PrefixExpressionNode, + PrimaryExpressionNode, + ProgramNode, + TupleExpressionNode, + VariableNode, + type SyntaxNode, +} from '@/core/types/nodes'; +import { type NodeSymbol, SchemaSymbol, SymbolKind } from '@/core/types/symbols'; +import Report from '@/core/types/report'; +import { DEFAULT_SCHEMA_NAME, UNHANDLED } from '@/constants'; +import { getBody, isAccessExpression, isExpressionAVariableNode } from '@/core/utils/expression'; +import { destructureComplexVariableTuple } from '@/core/syntax/utils'; +import type { TokenPosition } from '@/core/types/schemaJson'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; + +export function normalizeNoteContent (content: string): string { + const lines = content.split('\n'); + const trimmedTopEmptyLines = lines.slice(lines.findIndex((line) => line.trimStart() !== '')); + const nonEmptyLines = trimmedTopEmptyLines.filter((line) => line.trimStart()); + const minIndent = Math.min(...nonEmptyLines.map((line) => line.length - line.trimStart().length)); + return trimmedTopEmptyLines.map((line) => line.slice(minIndent)).join('\n'); +} + +function getDuplicateErrorInfo (member: NodeSymbol, name: string, compiler: Compiler): { code: CompileErrorCode; message: string; reportBoth: boolean } { + // Get schema context from the element's fullname + const fn = member.declaration ? compiler.fullname(member.declaration) : undefined; + const fullname = fn && !fn.hasValue(UNHANDLED) ? fn.getValue() : undefined; + const schemaLabel = fullname && fullname.length > 1 ? fullname.slice(0, -1).join('.') : 'public'; + + switch (member.kind) { + case SymbolKind.Table: + return { code: CompileErrorCode.DUPLICATE_NAME, message: `Table name '${name}' already exists in schema '${schemaLabel}'`, reportBoth: false }; + case SymbolKind.Enum: + return { code: CompileErrorCode.DUPLICATE_NAME, message: `Enum name ${name} already exists in schema '${schemaLabel}'`, reportBoth: false }; + case SymbolKind.Column: + return { code: CompileErrorCode.DUPLICATE_COLUMN_NAME, message: `Duplicate column ${name}`, reportBoth: true }; + case SymbolKind.EnumField: + return { code: CompileErrorCode.DUPLICATE_COLUMN_NAME, message: `Duplicate enum field ${name}`, reportBoth: true }; + case SymbolKind.TablePartial: + return { code: CompileErrorCode.DUPLICATE_NAME, message: `TablePartial name '${name}' already exists in schema '${schemaLabel}'`, reportBoth: false }; + case SymbolKind.TableGroup: + return { code: CompileErrorCode.DUPLICATE_NAME, message: `TableGroup name '${name}' already exists in schema '${schemaLabel}'`, reportBoth: false }; + default: + return { code: CompileErrorCode.DUPLICATE_NAME, message: `Duplicate ${member.kind} '${name}'`, reportBoth: true }; + } +} + +export function getTokenPosition (node: SyntaxNode): TokenPosition { + return { + start: { + offset: node.startPos.offset, + line: node.startPos.line + 1, + column: node.startPos.column + 1, + }, + end: { + offset: node.endPos.offset, + line: node.endPos.line + 1, + column: node.endPos.column + 1, + }, + }; +} + +// Kinds where duplicates are NOT allowed (same kind + same name = error) +const UNIQUE_KINDS = new Set([ + SymbolKind.Table, + SymbolKind.Enum, + SymbolKind.TableGroup, + SymbolKind.TableGroupField, + SymbolKind.EnumField, + SymbolKind.TablePartial, + SymbolKind.Column, + SymbolKind.Schema, + SymbolKind.Project, + SymbolKind.Note, +]); + +// Extract an element's members that is a direct syntactic child of the symbol's declaration body. +// Reports errors for duplicate symbols of the same kind and name. +export function getSymbolDirectMembers (compiler: Compiler, symbol: NodeSymbol): Report { + const node = symbol.declaration; + const children = node instanceof ElementDeclarationNode ? getBody(node) : (node instanceof ProgramNode ? node.body : undefined); + if (!children) { + return new Report([]); + } + + const membersReport = children.reduce( + (report, child) => { + const res = compiler.nodeSymbol(child); + return !res.hasValue(UNHANDLED) + ? report.chain((syms) => res.map((sym) => [...syms, sym])) + : report; + }, + new Report([]), + ); + + // Check for duplicates among unique kinds + const members = membersReport.getValue(); + const errors = [...membersReport.getErrors()]; + const seen = new Map(); // key: "kind:name" + + for (const member of members) { + if (!UNIQUE_KINDS.has(member.kind)) continue; + if (!member.declaration) continue; + + const nameResult = compiler.fullname(member.declaration); + if (nameResult.hasValue(UNHANDLED)) continue; + const fullname = nameResult.getValue(); + const name = fullname?.at(-1); + if (!name) continue; + + // Use full qualified name as key to distinguish auth.users from public.users + const key = `${member.kind}:${fullname?.join('.') ?? name}`; + const existing = seen.get(key); + if (existing) { + // Per-kind error formatting to match expected messages + const { code, message, reportBoth } = getDuplicateErrorInfo(member, name, compiler); + // Use the name node for the error location when available + const errorNode = (member.declaration instanceof ElementDeclarationNode && member.declaration.name) ? member.declaration.name : member.declaration; + errors.push(new CompileError(code, message, errorNode)); + if (reportBoth && existing.declaration) { + const existingErrorNode = (existing.declaration instanceof ElementDeclarationNode && existing.declaration.name) ? existing.declaration.name : existing.declaration; + errors.push(new CompileError(code, message, existingErrorNode)); + } + } else { + seen.set(key, member); + } + } + + return new Report(members, errors); +} + +export function getNodeMemberSymbols (compiler: Compiler, node: ElementDeclarationNode | ProgramNode): Report { + const children = node instanceof ElementDeclarationNode ? getBody(node) : (node instanceof ProgramNode ? node.body : undefined); + if (!children) { + return new Report([]); + } + + return children.reduce( + (report, child) => { + const symbol = compiler.nodeSymbol(child); + const nestedSymbols = compiler.nestedSymbols(child); + return new Report( + [ + ...report.getValue(), + ...(nestedSymbols.hasValue(UNHANDLED) ? [] : nestedSymbols.getValue()), + ], + [ + ...report.getErrors(), + ...(symbol.hasValue(UNHANDLED) ? [] : symbol.getErrors()), + ...(nestedSymbols.hasValue(UNHANDLED) ? [] : nestedSymbols.getErrors()), + ], + [ + ...report.getWarnings(), + ...(symbol.hasValue(UNHANDLED) ? [] : symbol.getWarnings()), + ...(nestedSymbols.hasValue(UNHANDLED) ? [] : nestedSymbols.getWarnings()), + + ], + ); + }, + new Report([]), + ); +} + +// Scan an AST node (excluding ListExpressions) for variable references. +// Returns structured binding fragments with dotted-path variables and tuple elements. +export function scanNonListNodeForBinding (node?: SyntaxNode): { variables: (PrimaryExpressionNode & { expression: VariableNode })[]; tupleElements: (PrimaryExpressionNode & { expression: VariableNode })[] }[] { + if (!node) return []; + + if (isExpressionAVariableNode(node)) { + return [{ variables: [node], tupleElements: [] }]; + } + + if (node instanceof InfixExpressionNode) { + const fragments = destructureComplexVariableTuple(node); + if (!fragments) { + return [...scanNonListNodeForBinding(node.leftExpression), ...scanNonListNodeForBinding(node.rightExpression)]; + } + return [fragments]; + } + + if (node instanceof PrefixExpressionNode) { + return scanNonListNodeForBinding(node.expression); + } + + if (node instanceof PostfixExpressionNode) { + return scanNonListNodeForBinding(node.expression); + } + + if (node instanceof TupleExpressionNode) { + const fragments = destructureComplexVariableTuple(node); + if (!fragments) { + return node.elementList.flatMap(scanNonListNodeForBinding); + } + return [fragments]; + } + + return []; +} + +// Look up a member by name within a parent symbol's members. +// Returns Report with the found symbol (or undefined) and any errors. +export function lookupMember ( + compiler: Compiler, + parentSymbol: NodeSymbol, + name: string, + { + kinds, + ignoreNotFound = false, + errorNode, + }: { + kinds?: SymbolKind[]; + ignoreNotFound?: boolean; + errorNode?: SyntaxNode; + } = {}, +): Report { + const members = compiler.symbolMembers(parentSymbol); + if (members.hasValue(UNHANDLED)) return new Report(undefined); + + const match = members.getValue().find((m: NodeSymbol) => { + if (kinds && !m.isKind(...kinds)) return false; + if (m instanceof SchemaSymbol) return m.name === name; + if (!m.declaration) return false; + const fn = compiler.fullname(m.declaration); + return !fn.hasValue(UNHANDLED) && fn.getValue()?.at(-1) === name; + }); + + if (!match && !ignoreNotFound) { + const kindLabel = kinds?.length ? kinds[0] : 'member'; + const fnResult = parentSymbol.declaration ? compiler.fullname(parentSymbol.declaration) : undefined; + const parentName = fnResult && !fnResult.hasValue(UNHANDLED) ? fnResult.getValue()?.join('.') : undefined; + const scopeLabel = parentSymbol instanceof SchemaSymbol ? `Schema '${parentSymbol.name}'` : parentName ? `${parentSymbol.kind} '${parentName}'` : (parentSymbol.isKind(SymbolKind.Program) ? `Schema '${DEFAULT_SCHEMA_NAME}'` : 'global scope'); + return new Report(undefined, [ + new CompileError(CompileErrorCode.BINDING_ERROR, `${kindLabel} '${name}' does not exist in ${scopeLabel}`, errorNode ?? parentSymbol.declaration ?? compiler.parseFile().getValue().ast), + ]); + } + + return new Report(match); +} + +// Look up a member in the default (public) schema, falling back to direct program search +export function lookupInDefaultSchema (compiler: Compiler, globalSymbol: NodeSymbol, name: string, opts: { kinds?: SymbolKind[]; ignoreNotFound?: boolean; errorNode?: SyntaxNode }): Report { + const members = compiler.symbolMembers(globalSymbol); + if (!members.hasValue(UNHANDLED)) { + const publicSchema = members.getValue().find((m: NodeSymbol) => m instanceof SchemaSymbol && m.name === DEFAULT_SCHEMA_NAME); + if (publicSchema) { + const result = lookupMember(compiler, publicSchema, name, { ...opts, ignoreNotFound: true }); + if (result.getValue()) return result; + } + } + return lookupMember(compiler, globalSymbol, name, opts); +} + +// For a node that is the right side of an access expression (a.b), +// resolve the left side via compiler.nodeReferee and return its symbol. +export function nodeRefereeOfLeftExpression (compiler: Compiler, node: SyntaxNode): NodeSymbol | undefined { + const parent = node.parent; + if (!parent || !isAccessExpression(parent) || parent.rightExpression !== node) return undefined; + const result = compiler.nodeReferee(parent.leftExpression); + if (result.hasValue(UNHANDLED)) return undefined; + return result.getValue() ?? undefined; +} diff --git a/packages/dbml-parse/src/core/interpreter/elementInterpreter/enum.ts b/packages/dbml-parse/src/core/interpreter/elementInterpreter/enum.ts deleted file mode 100644 index 2c7a13bb4..000000000 --- a/packages/dbml-parse/src/core/interpreter/elementInterpreter/enum.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { extractQuotedStringToken, extractVariableFromExpression } from '@/core/analyzer/utils'; -import { aggregateSettingList } from '@/core/analyzer/validator/utils'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { - ElementInterpreter, Enum, EnumField, InterpreterDatabase, -} from '@/core/interpreter/types'; -import { extractElementName, getTokenPosition, normalizeNoteContent } from '@/core/interpreter/utils'; - -export class EnumInterpreter implements ElementInterpreter { - private declarationNode: ElementDeclarationNode; - private env: InterpreterDatabase; - private enum: Partial; - - constructor (declarationNode: ElementDeclarationNode, env: InterpreterDatabase) { - this.declarationNode = declarationNode; - this.env = env; - this.enum = { values: [] }; - } - - interpret (): CompileError[] { - this.enum.token = getTokenPosition(this.declarationNode); - this.env.enums.set(this.declarationNode, this.enum as Enum); - const errors = [...this.interpretName(this.declarationNode.name!), ...this.interpretBody(this.declarationNode.body as BlockExpressionNode)]; - - return errors; - } - - private interpretName (nameNode: SyntaxNode): CompileError[] { - const { name, schemaName } = extractElementName(nameNode); - - if (schemaName.length > 1) { - this.enum.name = name; - this.enum.schemaName = schemaName.join('.'); - - return [new CompileError(CompileErrorCode.UNSUPPORTED, 'Nested schema is not supported', nameNode)]; - } - - this.enum.name = name; - this.enum.schemaName = schemaName.length ? schemaName[0] : null; - - return []; - } - - private interpretBody (body: BlockExpressionNode): CompileError[] { - return body.body.flatMap((_field) => { - const field = _field as FunctionApplicationNode; - - const enumField: Partial = { }; - - enumField.token = getTokenPosition(field); - enumField.name = extractVariableFromExpression(field.callee).unwrap(); - - const settingMap = aggregateSettingList(field.args[0] as ListExpressionNode).getValue(); - const noteNode = settingMap.note?.at(0); - enumField.note = noteNode && { - value: extractQuotedStringToken(noteNode.value).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(noteNode), - }; - - this.enum.values!.push(enumField as EnumField); - - return []; - }); - } -} diff --git a/packages/dbml-parse/src/core/interpreter/elementInterpreter/project.ts b/packages/dbml-parse/src/core/interpreter/elementInterpreter/project.ts deleted file mode 100644 index 9d014fbe9..000000000 --- a/packages/dbml-parse/src/core/interpreter/elementInterpreter/project.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { extractQuotedStringToken } from '@/core/analyzer/utils'; -import { CompileError } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { ElementInterpreter, InterpreterDatabase, Project } from '@/core/interpreter/types'; -import { extractElementName, getTokenPosition, normalizeNoteContent } from '@/core/interpreter/utils'; -import { EnumInterpreter } from './enum'; -import { RefInterpreter } from './ref'; -import { TableInterpreter } from './table'; -import { TableGroupInterpreter } from './tableGroup'; -import { TablePartialInterpreter } from './tablePartial'; - -export class ProjectInterpreter implements ElementInterpreter { - private declarationNode: ElementDeclarationNode; - private env: InterpreterDatabase; - private project: Partial; - - constructor (declarationNode: ElementDeclarationNode, env: InterpreterDatabase) { - this.declarationNode = declarationNode; - this.env = env; - this.project = { - enums: [], refs: [], tableGroups: [], tables: [], tablePartials: [], - }; - } - - interpret (): CompileError[] { - this.env.project.set(this.declarationNode, this.project as Project); - this.project.token = getTokenPosition(this.declarationNode); - const errors = [...this.interpretName(this.declarationNode.name), ...this.interpretBody(this.declarationNode.body as BlockExpressionNode)]; - - return errors; - } - - private interpretName (nameNode?: SyntaxNode): CompileError[] { - if (!nameNode) { - this.project.name = null; - - return []; - } - - const { name } = extractElementName(nameNode); - this.project.name = name; - - return []; - } - - private interpretBody (body: BlockExpressionNode): CompileError[] { - return body.body.flatMap((_sub) => { - const sub = _sub as ElementDeclarationNode; - switch (sub.type?.value.toLowerCase()) { - case 'table': { - const errors = (new TableInterpreter(sub, this.env)).interpret(); - this.project.tables!.push(this.env.tables.get(sub)!); - - return errors; - } - case 'ref': { - const errors = (new RefInterpreter(sub, this.env)).interpret(); - this.project.refs!.push(this.env.ref.get(sub)!); - - return errors; - } - case 'tablegroup': { - const errors = (new TableGroupInterpreter(sub, this.env)).interpret(); - this.project.tableGroups!.push(this.env.tableGroups.get(sub)!); - - return errors; - } - case 'enum': { - const errors = (new EnumInterpreter(sub, this.env)).interpret(); - this.project.enums!.push(this.env.enums.get(sub)!); - - return errors; - } - case 'note': { - this.project.note = { - value: extractQuotedStringToken( - sub.body instanceof BlockExpressionNode - ? (sub.body.body[0] as FunctionApplicationNode).callee - : sub.body!.callee, - ).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(sub), - }; - return []; - } - case 'tablepartial': { - const errors = (new TablePartialInterpreter(sub, this.env)).interpret(); - this.project.tablePartials!.push(this.env.tablePartials.get(sub)!); - return errors; - } - default: { - (this.project as any)[sub.type!.value.toLowerCase()] = extractQuotedStringToken((sub.body as FunctionApplicationNode).callee).unwrap(); - - return []; - } - } - }); - } -} diff --git a/packages/dbml-parse/src/core/interpreter/elementInterpreter/ref.ts b/packages/dbml-parse/src/core/interpreter/elementInterpreter/ref.ts deleted file mode 100644 index 3d8ba2f22..000000000 --- a/packages/dbml-parse/src/core/interpreter/elementInterpreter/ref.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { destructureComplexVariable, extractVariableFromExpression } from '@/core/analyzer/utils'; -import { aggregateSettingList } from '@/core/analyzer/validator/utils'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, IdentiferStreamNode, InfixExpressionNode, ListExpressionNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { - ElementInterpreter, InterpreterDatabase, Ref, Table, -} from '@/core/interpreter/types'; -import { - extractColor, extractNamesFromRefOperand, getColumnSymbolsOfRefOperand, getMultiplicities, getRefId, getTokenPosition, isSameEndpoint, -} from '@/core/interpreter/utils'; -import { extractStringFromIdentifierStream } from '@/core/parser/utils'; - -export class RefInterpreter implements ElementInterpreter { - private declarationNode: ElementDeclarationNode; - private env: InterpreterDatabase; - private container: Partial
| undefined; - private ref: Partial; - - constructor (declarationNode: ElementDeclarationNode, env: InterpreterDatabase) { - this.declarationNode = declarationNode; - this.env = env; - this.container = this.declarationNode.parent instanceof ElementDeclarationNode ? this.env.tables.get(this.declarationNode.parent) : undefined; - this.ref = { }; - } - - interpret (): CompileError[] { - this.ref.token = getTokenPosition(this.declarationNode); - this.env.ref.set(this.declarationNode, this.ref as Ref); - const errors = [ - ...this.interpretName(this.declarationNode.name!), - ...this.interpretBody(this.declarationNode.body!), - ]; - return errors; - } - - private interpretName (_nameNode: SyntaxNode): CompileError[] { - const errors: CompileError[] = []; - - const fragments = destructureComplexVariable(this.declarationNode.name!).unwrap_or([]); - this.ref.name = fragments.pop() || null; - if (fragments.length > 1) { - errors.push(new CompileError(CompileErrorCode.UNSUPPORTED, 'Nested schema is not supported', this.declarationNode.name!)); - } - this.ref.schemaName = fragments.join('.') || null; - - return errors; - } - - private interpretBody (body: FunctionApplicationNode | BlockExpressionNode): CompileError[] { - if (body instanceof FunctionApplicationNode) { - return this.interpretField(body); - } - - return this.interpretField(body.body[0] as FunctionApplicationNode); - } - - private interpretField (field: FunctionApplicationNode): CompileError[] { - const op = (field.callee as InfixExpressionNode).op!.value; - const { leftExpression, rightExpression } = field.callee as InfixExpressionNode; - - const leftSymbols = getColumnSymbolsOfRefOperand(leftExpression!); - const rightSymbols = getColumnSymbolsOfRefOperand(rightExpression!); - - if (isSameEndpoint(leftSymbols, rightSymbols)) { - return [new CompileError(CompileErrorCode.SAME_ENDPOINT, 'Two endpoints are the same', field)]; - } - - const refId = getRefId(leftSymbols, rightSymbols); - if (this.env.refIds[refId]) { - return [ - new CompileError(CompileErrorCode.CIRCULAR_REF, 'References with same endpoints exist', this.declarationNode), - new CompileError(CompileErrorCode.CIRCULAR_REF, 'References with same endpoints exist', this.env.refIds[refId]), - ]; - } - - if (field.args[0]) { - const settingMap = aggregateSettingList(field.args[0] as ListExpressionNode).getValue(); - - const deleteSetting = settingMap.delete?.at(0)?.value; - this.ref.onDelete = deleteSetting instanceof IdentiferStreamNode - ? extractStringFromIdentifierStream(deleteSetting).unwrap_or(undefined) - : extractVariableFromExpression(deleteSetting).unwrap_or(undefined) as string; - - const updateSetting = settingMap.update?.at(0)?.value; - this.ref.onUpdate = updateSetting instanceof IdentiferStreamNode - ? extractStringFromIdentifierStream(updateSetting).unwrap_or(undefined) - : extractVariableFromExpression(updateSetting).unwrap_or(undefined) as string; - - this.ref.color = settingMap.color?.length ? extractColor(settingMap.color?.at(0)?.value as any) : undefined; - } - - const multiplicities = getMultiplicities(op); - - this.ref.endpoints = [ - { - ...extractNamesFromRefOperand(leftExpression!, this.container as Table | undefined), - relation: multiplicities[0], - token: getTokenPosition(leftExpression!), - }, - { - ...extractNamesFromRefOperand(rightExpression!, this.container as Table | undefined), - relation: multiplicities[1], - token: getTokenPosition(rightExpression!), - }, - ]; - - this.env.refIds[refId] = this.declarationNode; - - return []; - } -} diff --git a/packages/dbml-parse/src/core/interpreter/elementInterpreter/sticky_note.ts b/packages/dbml-parse/src/core/interpreter/elementInterpreter/sticky_note.ts deleted file mode 100644 index 0a4cc1ab8..000000000 --- a/packages/dbml-parse/src/core/interpreter/elementInterpreter/sticky_note.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { partition, get } from 'lodash-es'; -import { ElementInterpreter, InterpreterDatabase, Note } from '@/core/interpreter/types'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { - extractColor, extractElementName, getTokenPosition, normalizeNoteContent, -} from '@/core/interpreter/utils'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { aggregateSettingList } from '@/core/analyzer/validator/utils'; - -export class StickyNoteInterpreter implements ElementInterpreter { - private declarationNode: ElementDeclarationNode; - private env: InterpreterDatabase; - private note: Partial; - - constructor (declarationNode: ElementDeclarationNode, env: InterpreterDatabase) { - this.declarationNode = declarationNode; - this.env = env; - this.note = { name: undefined, content: undefined, token: undefined }; - } - - interpret (): CompileError[] { - this.note.token = getTokenPosition(this.declarationNode); - this.env.notes.set(this.declarationNode, this.note as Note); - - const errors = [ - ...this.interpretName(this.declarationNode.name!), - ...this.interpretSettingList(this.declarationNode.attributeList), - ...this.interpretBody(this.declarationNode.body as BlockExpressionNode), - ]; - - return errors; - } - - private interpretName (nameNode: SyntaxNode): CompileError[] { - const { name } = extractElementName(nameNode); - - this.note.name = name; - - return []; - } - - private interpretSettingList (settings?: ListExpressionNode): CompileError[] { - const settingMap = aggregateSettingList(settings).getValue(); - - this.note.headerColor = settingMap.headercolor?.length ? extractColor(settingMap.headercolor?.at(0)?.value as any) : undefined; - - return []; - } - - private interpretBody (body: BlockExpressionNode): CompileError[] { - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - - if (fields.length !== 1 || subs.length > 0) { - return [ - new CompileError(CompileErrorCode.INVALID_NOTE, 'Invalid note syntax', body), - ]; - } - - return [...this.interpretNote(fields[0] as FunctionApplicationNode)]; - } - - private interpretNote (note: FunctionApplicationNode): CompileError[] { - const noteContent = get(note, 'callee.expression.literal.value', ''); - - this.note.content = normalizeNoteContent(noteContent); - return []; - } -} diff --git a/packages/dbml-parse/src/core/interpreter/elementInterpreter/table.ts b/packages/dbml-parse/src/core/interpreter/elementInterpreter/table.ts deleted file mode 100644 index 440ad3d2f..000000000 --- a/packages/dbml-parse/src/core/interpreter/elementInterpreter/table.ts +++ /dev/null @@ -1,431 +0,0 @@ -import { partition, last } from 'lodash-es'; -import { - Column, Check, ElementInterpreter, Index, InlineRef, - InterpreterDatabase, Table, TablePartialInjection, -} from '@/core/interpreter/types'; -import { - AttributeNode, BlockExpressionNode, CallExpressionNode, ElementDeclarationNode, - FunctionApplicationNode, FunctionExpressionNode, ListExpressionNode, PrefixExpressionNode, - SyntaxNode, -} from '@/core/parser/nodes'; -import { - extractColor, extractElementName, getColumnSymbolsOfRefOperand, getMultiplicities, - getRefId, getTokenPosition, isSameEndpoint, normalizeNoteContent, - processColumnType, processDefaultValue, -} from '@/core/interpreter/utils'; -import { - destructureComplexVariable, destructureIndexNode, extractQuotedStringToken, extractVarNameFromPrimaryVariable, - extractVariableFromExpression, -} from '@/core/analyzer/utils'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { aggregateSettingList, isValidPartialInjection } from '@/core/analyzer/validator/utils'; -import { ColumnSymbol } from '@/core/analyzer/symbol/symbols'; -import { destructureIndex, SymbolKind } from '@/core/analyzer/symbol/symbolIndex'; -import { ElementKind, SettingName } from '@/core/analyzer/types'; - -export class TableInterpreter implements ElementInterpreter { - private declarationNode: ElementDeclarationNode; - private env: InterpreterDatabase; - private table: Partial
; - private pkColumns: Column[]; - - constructor (declarationNode: ElementDeclarationNode, env: InterpreterDatabase) { - this.declarationNode = declarationNode; - this.env = env; - this.table = { - name: undefined, - schemaName: undefined, - alias: null, - fields: [], - token: undefined, - indexes: [], - partials: [], - checks: [], - }; - this.pkColumns = []; - } - - interpret (): CompileError[] { - this.table.token = getTokenPosition(this.declarationNode); - this.env.tables.set(this.declarationNode, this.table as Table); - - const errors = [ - ...this.interpretName(this.declarationNode.name!), - ...this.interpretAlias(this.declarationNode.alias), - ...this.interpretSettingList(this.declarationNode.attributeList), - ...this.interpretBody(this.declarationNode.body as BlockExpressionNode), - ]; - - // Handle cases where there are multiple primary columns - // all the pk field of the columns are reset to false - // and a new pk composite index is added - if (this.pkColumns.length >= 2) { - this.table.indexes!.push({ - columns: this.pkColumns.map(({ name, token }) => ({ value: name, type: 'column', token })), - token: { - start: { offset: -1, line: -1, column: -1 }, // do not make sense to have a meaningful start (?) - end: { offset: -1, line: -1, column: -1 }, // do not make sense to have a meaningful end (?) - }, - pk: true, - }); - this.pkColumns.forEach((column) => { - column.pk = false; - }); - } - - return errors; - } - - private interpretName (nameNode: SyntaxNode): CompileError[] { - const { name, schemaName } = extractElementName(nameNode); - - if (schemaName.length > 1) { - this.table.name = name; - this.table.schemaName = schemaName.join('.'); - - return [new CompileError(CompileErrorCode.UNSUPPORTED, 'Nested schema is not supported', nameNode)]; - } - - this.table.name = name; - this.table.schemaName = schemaName.length ? schemaName[0] : null; - - return []; - } - - private interpretAlias (aliasNode?: SyntaxNode): CompileError[] { - if (!aliasNode) { - return []; - } - - const alias = extractVarNameFromPrimaryVariable(aliasNode as any).unwrap_or(null); - if (alias) { - this.env.aliases.push({ - name: alias, - kind: 'table', - value: { - tableName: this.table.name!, - schemaName: this.table.schemaName!, - }, - }); - this.table.alias = alias; - } - - return []; - } - - private interpretSettingList (settings?: ListExpressionNode): CompileError[] { - const settingMap = aggregateSettingList(settings).getValue(); - - this.table.headerColor = settingMap[SettingName.HeaderColor]?.length - ? extractColor(settingMap[SettingName.HeaderColor]?.at(0)?.value as any) - : undefined; - - const [noteNode] = settingMap[SettingName.Note] || []; - this.table.note = noteNode && { - value: extractQuotedStringToken(noteNode?.value).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(noteNode), - }; - - return []; - } - - private interpretBody (body: BlockExpressionNode): CompileError[] { - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [ - ...this.interpretFields(fields as FunctionApplicationNode[]), - ...this.interpretSubElements(subs as ElementDeclarationNode[]), - ]; - } - - private interpretSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - switch (sub.type?.value.toLowerCase()) { - case ElementKind.Note: - this.table.note = { - value: extractQuotedStringToken( - sub.body instanceof BlockExpressionNode - ? (sub.body.body[0] as FunctionApplicationNode).callee - : sub.body!.callee, - ).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(sub), - }; - return []; - - case ElementKind.Indexes: - return this.interpretIndexes(sub); - - case ElementKind.Check: - return this.interpretChecks(sub); - - case ElementKind.Records: - // Collect nested records for later interpretation - this.env.recordsElements.push(sub); - return []; - - default: - return []; - } - }); - } - - private interpretInjection (injection: PrefixExpressionNode, order: number) { - const partial: Partial = { order, token: getTokenPosition(injection) }; - partial.name = extractVariableFromExpression(injection.expression).unwrap_or(''); - this.table.partials!.push(partial as TablePartialInjection); - return []; - } - - private interpretFields (fields: FunctionApplicationNode[]): CompileError[] { - const symbolTableEntries = this.declarationNode.symbol?.symbolTable - ? [...this.declarationNode.symbol.symbolTable.entries()] - : []; - const columnEntries = symbolTableEntries.filter(([index]) => { - const res = destructureIndex(index).unwrap_or(null); - return res?.kind === SymbolKind.Column; - }); - - const columnCountErrors = columnEntries.length - ? [] - : [new CompileError(CompileErrorCode.EMPTY_TABLE, 'A Table must have at least one column', this.declarationNode)]; - - const interpretFieldErrors = fields.flatMap((field, order) => { - return isValidPartialInjection(field.callee) - ? this.interpretInjection(field.callee, order) - : this.interpretColumn(field); - }); - - return [ - ...columnCountErrors, - ...interpretFieldErrors, - ]; - } - - private interpretColumn (field: FunctionApplicationNode): CompileError[] { - const errors: CompileError[] = []; - - const column: Partial = {}; - - column.name = extractVarNameFromPrimaryVariable(field.callee as any).unwrap(); - - const typeReport = processColumnType(field.args[0], this.env); - column.type = typeReport.getValue(); - errors.push(...typeReport.getErrors()); - - column.token = getTokenPosition(field); - column.inline_refs = []; - - const settings = field.args.slice(1); - if (last(settings) instanceof ListExpressionNode) { - const settingMap = aggregateSettingList(settings.pop() as ListExpressionNode).getValue(); - - column.pk = !!settingMap[SettingName.PK]?.length || !!settingMap[SettingName.PrimaryKey]?.length; - column.increment = !!settingMap[SettingName.Increment]?.length; - column.unique = !!settingMap[SettingName.Unique]?.length; - - column.not_null = settingMap[SettingName.NotNull]?.length - ? true - : settingMap[SettingName.Null]?.length - ? false - : undefined; - column.dbdefault = processDefaultValue(settingMap[SettingName.Default]?.at(0)?.value); - - const noteNode = settingMap[SettingName.Note]?.at(0); - column.note = noteNode && { - value: extractQuotedStringToken(noteNode.value).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(noteNode), - }; - - const refs = settingMap[SettingName.Ref] || []; - column.inline_refs = refs.flatMap((ref) => { - const [referredSymbol] = getColumnSymbolsOfRefOperand((ref.value as PrefixExpressionNode).expression!); - - if (isSameEndpoint(referredSymbol, field.symbol as ColumnSymbol)) { - errors.push(new CompileError(CompileErrorCode.SAME_ENDPOINT, 'Two endpoints are the same', ref)); - - return []; - } - - const op = (ref.value as PrefixExpressionNode).op!; - const fragments = destructureComplexVariable((ref.value as PrefixExpressionNode).expression).unwrap(); - - let inlineRef: InlineRef | undefined; - if (fragments.length === 1) { - const [columnName] = fragments; - - inlineRef = { - schemaName: this.table.schemaName!, - tableName: this.table.name!, - fieldNames: [columnName], - relation: op.value as any, - token: getTokenPosition(ref), - }; - } else if (fragments.length === 2) { - const [table, columnName] = fragments; - inlineRef = { - schemaName: null, - tableName: table, - fieldNames: [columnName], - relation: op.value as any, - token: getTokenPosition(ref), - }; - } else if (fragments.length === 3) { - const [schema, table, columnName] = fragments; - inlineRef = { - schemaName: schema, - tableName: table, - fieldNames: [columnName], - relation: op.value as any, - token: getTokenPosition(ref), - }; - } else { - errors.push(new CompileError(CompileErrorCode.UNSUPPORTED, 'Nested schema is not supported', ref)); - const columnName = fragments.pop()!; - const table = fragments.pop()!; - const schema = fragments.join('.'); - inlineRef = { - schemaName: schema, - tableName: table, - fieldNames: [columnName], - relation: op.value as any, - token: getTokenPosition(ref), - }; - } - - const errs = this.registerInlineRefToEnv(field, referredSymbol, inlineRef, ref); - errors.push(...errs); - - return errs.length === 0 ? inlineRef : []; - }); - - const checkNodes = settingMap[SettingName.Check] || []; - column.checks = checkNodes.map((checkNode) => { - const token = getTokenPosition(checkNode); - const expression = (checkNode.value as FunctionExpressionNode).value!.value!; - return { - token, - expression, - }; - }); - } - - column.pk ||= settings.some((setting) => extractVariableFromExpression(setting).unwrap().toLowerCase() === 'pk'); - column.unique ||= settings.some((setting) => extractVariableFromExpression(setting).unwrap().toLowerCase() === 'unique'); - - this.table.fields!.push(column as Column); - if (column.pk) { - this.pkColumns.push(column as Column); - } - - return errors; - } - - private interpretIndexes (indexes: ElementDeclarationNode): CompileError[] { - this.table.indexes!.push(...(indexes.body as BlockExpressionNode).body.map((_indexField) => { - const index: Partial = { columns: [] }; - - const indexField = _indexField as FunctionApplicationNode; - index.token = getTokenPosition(indexField); - const args = [indexField.callee!, ...indexField.args]; - if (last(args) instanceof ListExpressionNode) { - const settingMap = aggregateSettingList(args.pop() as ListExpressionNode).getValue(); - index.pk = !!settingMap[SettingName.PK]?.length; - index.unique = !!settingMap[SettingName.Unique]?.length; - index.name = extractQuotedStringToken(settingMap[SettingName.Name]?.at(0)?.value).unwrap_or(undefined); - const noteNode = settingMap[SettingName.Note]?.at(0); - index.note = noteNode && { - value: extractQuotedStringToken(noteNode.value).unwrap(), - token: getTokenPosition(noteNode), - }; - index.type = extractVariableFromExpression(settingMap[SettingName.Type]?.at(0)?.value).unwrap_or(undefined); - } - - args.flatMap((arg) => { - // This is to deal with indexes fields such as - // (id, name) (age, weight) - // which is a call expression - if (!(arg instanceof CallExpressionNode)) { - return arg; - } - const fragments: SyntaxNode[] = []; - while (arg instanceof CallExpressionNode) { - fragments.push(arg.argumentList!); - - arg = arg.callee!; - } - fragments.push(arg); - - return fragments; - }).forEach((arg) => { - const { functional, nonFunctional } = destructureIndexNode(arg).unwrap(); - index.columns!.push( - ...functional.map((s) => ({ - value: s.value!.value, - type: 'expression', - token: getTokenPosition(s), - })), - ...nonFunctional.map((s) => ({ - value: extractVarNameFromPrimaryVariable(s).unwrap(), - type: 'column', - token: getTokenPosition(s), - })), - ); - }); - - return index as Index; - })); - - return []; - } - - private interpretChecks (checks: ElementDeclarationNode): CompileError[] { - this.table.checks!.push(...(checks.body as BlockExpressionNode).body.map((_checkField) => { - const check: Partial = {}; - const checkField = _checkField as FunctionApplicationNode; - check.token = getTokenPosition(checkField); - - if (checkField.args[0] instanceof ListExpressionNode) { - const settingMap = aggregateSettingList(checkField.args[0] as ListExpressionNode).getValue(); - check.name = extractQuotedStringToken(settingMap[SettingName.Name]?.at(0)?.value).unwrap_or(undefined); - } - - check.expression = (checkField.callee as FunctionExpressionNode).value!.value!; - - return check as Check; - })); - - return []; - } - - private registerInlineRefToEnv (column: FunctionApplicationNode, referredSymbol: ColumnSymbol, inlineRef: InlineRef, ref: AttributeNode): CompileError[] { - const refId = getRefId(column.symbol as ColumnSymbol, referredSymbol); - if (this.env.refIds[refId]) { - return [ - new CompileError(CompileErrorCode.CIRCULAR_REF, 'References with same endpoints exist', ref), - new CompileError(CompileErrorCode.CIRCULAR_REF, 'References with same endpoints exist', this.env.refIds[refId]), - ]; - } - - const multiplicities = getMultiplicities(inlineRef.relation); - this.env.refIds[refId] = ref; - this.env.ref.set(ref, { - name: null, - schemaName: null, - token: inlineRef.token, - endpoints: [ - { - ...inlineRef, - relation: multiplicities[1], - }, - { - schemaName: this.table.schemaName!, - tableName: this.table.name!, - fieldNames: [extractVariableFromExpression(column.callee!).unwrap()], - token: getTokenPosition(column), - relation: multiplicities[0], - }, - ], - }); - - return []; - } -} diff --git a/packages/dbml-parse/src/core/interpreter/elementInterpreter/tableGroup.ts b/packages/dbml-parse/src/core/interpreter/elementInterpreter/tableGroup.ts deleted file mode 100644 index 010ab3f70..000000000 --- a/packages/dbml-parse/src/core/interpreter/elementInterpreter/tableGroup.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { partition } from 'lodash-es'; -import { destructureComplexVariable, destructureMemberAccessExpression, extractQuotedStringToken } from '@/core/analyzer/utils'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { - BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, SyntaxNode, ListExpressionNode, -} from '@/core/parser/nodes'; -import { ElementInterpreter, InterpreterDatabase, TableGroup } from '@/core/interpreter/types'; -import { - extractElementName, getTokenPosition, normalizeNoteContent, extractColor, -} from '@/core/interpreter/utils'; -import { aggregateSettingList } from '@/core/analyzer/validator/utils'; - -export class TableGroupInterpreter implements ElementInterpreter { - private declarationNode: ElementDeclarationNode; - private env: InterpreterDatabase; - private tableGroup: Partial; - - constructor (declarationNode: ElementDeclarationNode, env: InterpreterDatabase) { - this.declarationNode = declarationNode; - this.env = env; - this.tableGroup = { tables: [] }; - } - - interpret (): CompileError[] { - const errors: CompileError[] = []; - this.tableGroup.token = getTokenPosition(this.declarationNode); - this.env.tableGroups.set(this.declarationNode, this.tableGroup as TableGroup); - - errors.push( - ...this.interpretName(this.declarationNode.name!), - ...this.interpretSettingList(this.declarationNode.attributeList), - ...this.interpretBody(this.declarationNode.body as BlockExpressionNode), - ); - - return errors; - } - - private interpretName (nameNode: SyntaxNode): CompileError[] { - const errors: CompileError[] = []; - - const { name, schemaName } = extractElementName(nameNode); - if (schemaName.length >= 2) { - this.tableGroup.name = name; - this.tableGroup.schemaName = schemaName.join('.'); - errors.push(new CompileError(CompileErrorCode.UNSUPPORTED, 'Nested schema is not supported', this.declarationNode.name!)); - } - this.tableGroup.name = name; - this.tableGroup.schemaName = schemaName[0] || null; - - return errors; - } - - private interpretBody (body: BlockExpressionNode): CompileError[] { - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [ - ...this.interpretFields(fields as FunctionApplicationNode[]), - ...this.interpretSubElements(subs as ElementDeclarationNode[]), - ]; - } - - private interpretSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - switch (sub.type?.value.toLowerCase()) { - case 'note': - this.tableGroup.note = { - value: extractQuotedStringToken( - sub.body instanceof BlockExpressionNode - ? (sub.body.body[0] as FunctionApplicationNode).callee - : sub.body!.callee, - ) - .map(normalizeNoteContent) - .unwrap(), - token: getTokenPosition(sub), - }; - break; - - default: - break; - } - - return []; - }); - } - - private interpretFields (fields: FunctionApplicationNode[]): CompileError[] { - const errors: CompileError[] = []; - this.tableGroup.tables = fields.map((field) => { - const fragments = destructureComplexVariable((field as FunctionApplicationNode).callee).unwrap(); - - if (fragments.length > 2) { - errors.push(new CompileError(CompileErrorCode.UNSUPPORTED, 'Nested schema is not supported', field)); - } - - const tableid = destructureMemberAccessExpression((field as FunctionApplicationNode).callee!).unwrap().pop()!.referee!.id; - if (this.env.tableOwnerGroup[tableid]) { - const tableGroup = this.env.tableOwnerGroup[tableid]; - const { schemaName, name } = this.env.tableGroups.get(tableGroup)!; - const groupName = schemaName ? `${schemaName}.${name}` : name; - errors.push(new CompileError(CompileErrorCode.TABLE_REAPPEAR_IN_TABLEGROUP, `Table "${fragments.join('.')}" already appears in group "${groupName}"`, field)); - } else { - this.env.tableOwnerGroup[tableid] = this.declarationNode; - } - - return { - name: fragments.pop()!, - schemaName: fragments.join('.'), - }; - }); - - return errors; - } - - private interpretSettingList (settings?: ListExpressionNode): CompileError[] { - const settingMap = aggregateSettingList(settings).getValue(); - - this.tableGroup.color = settingMap.color?.length - ? extractColor(settingMap.color?.at(0)?.value as any) - : undefined; - - const [noteNode] = settingMap.note || []; - this.tableGroup.note = noteNode && { - value: extractQuotedStringToken(noteNode?.value).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(noteNode), - }; - - return []; - } -} diff --git a/packages/dbml-parse/src/core/interpreter/elementInterpreter/tablePartial.ts b/packages/dbml-parse/src/core/interpreter/elementInterpreter/tablePartial.ts deleted file mode 100644 index 9ba68a3eb..000000000 --- a/packages/dbml-parse/src/core/interpreter/elementInterpreter/tablePartial.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { last, head, partition } from 'lodash-es'; -import { - Column, Check, ElementInterpreter, Index, InlineRef, - InterpreterDatabase, TablePartial, -} from '@/core/interpreter/types'; -import { - BlockExpressionNode, CallExpressionNode, ElementDeclarationNode, FunctionApplicationNode, - FunctionExpressionNode, - ListExpressionNode, PrefixExpressionNode, SyntaxNode, -} from '@/core/parser/nodes'; -import { - extractColor, extractElementName, getColumnSymbolsOfRefOperand, getTokenPosition, - isSameEndpoint, normalizeNoteContent, processColumnType, processDefaultValue, -} from '@/core/interpreter/utils'; -import { - destructureComplexVariable, destructureIndexNode, extractQuotedStringToken, extractVarNameFromPrimaryVariable, - extractVariableFromExpression, -} from '@/core/analyzer/utils'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { aggregateSettingList } from '@/core/analyzer/validator/utils'; -import { ColumnSymbol } from '@/core/analyzer/symbol/symbols'; -import { ElementKind, SettingName } from '@/core/analyzer/types'; - -export class TablePartialInterpreter implements ElementInterpreter { - private declarationNode: ElementDeclarationNode; - private env: InterpreterDatabase; - private tablePartial: Partial; - private pkColumns: Column[]; - - constructor (declarationNode: ElementDeclarationNode, env: InterpreterDatabase) { - this.declarationNode = declarationNode; - this.env = env; - this.tablePartial = { - name: undefined, fields: [], token: undefined, indexes: [], checks: [], - }; - this.pkColumns = []; - } - - interpret (): CompileError[] { - this.tablePartial.token = getTokenPosition(this.declarationNode); - this.env.tablePartials.set(this.declarationNode, this.tablePartial as TablePartial); - - const errors = [ - ...this.interpretName(this.declarationNode.name!), - ...this.interpretSettingList(this.declarationNode.attributeList), - ...this.interpretBody(this.declarationNode.body as BlockExpressionNode), - ]; - - // Handle cases where there are multiple primary columns - // all the pk field of the columns are reset to false - // and a new pk composite index is added - if (this.pkColumns.length >= 2) { - this.tablePartial.indexes!.push({ - columns: this.pkColumns.map(({ name, token }) => ({ value: name, type: 'column', token })), - token: { - start: { offset: -1, line: -1, column: -1 }, // do not make sense to have a meaningful start (?) - end: { offset: -1, line: -1, column: -1 }, // do not make sense to have a meaningful end (?) - }, - pk: true, - }); - this.pkColumns.forEach((column) => { - column.pk = false; - }); - } - - return errors; - } - - private interpretName (nameNode: SyntaxNode): CompileError[] { - const { name } = extractElementName(nameNode); - - this.tablePartial.name = name; - - return []; - } - - private interpretSettingList (settings?: ListExpressionNode): CompileError[] { - const settingMap = aggregateSettingList(settings).getValue(); - - const firstHeaderColor = head(settingMap[SettingName.HeaderColor]); - this.tablePartial.headerColor = firstHeaderColor - ? extractColor(firstHeaderColor.value as any) - : undefined; - - const [noteNode] = settingMap[SettingName.Note] || []; - this.tablePartial.note = noteNode && { - value: extractQuotedStringToken(noteNode?.value).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(noteNode), - }; - - return []; - } - - private interpretBody (body: BlockExpressionNode): CompileError[] { - const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); - return [ - ...this.interpretFields(fields as FunctionApplicationNode[]), - ...this.interpretSubElements(subs as ElementDeclarationNode[]), - ]; - } - - private interpretSubElements (subs: ElementDeclarationNode[]): CompileError[] { - return subs.flatMap((sub) => { - switch (sub.type?.value.toLowerCase()) { - case ElementKind.Note: - this.tablePartial.note = { - value: extractQuotedStringToken( - sub.body instanceof BlockExpressionNode - ? (sub.body.body[0] as FunctionApplicationNode).callee - : sub.body!.callee, - ).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(sub), - }; - return []; - - case ElementKind.Indexes: - return this.interpretIndexes(sub); - - case ElementKind.Check: - return this.interpretChecks(sub); - - default: - return []; - } - }); - } - - private interpretFields (fields: FunctionApplicationNode[]): CompileError[] { - return fields.flatMap((field) => this.interpretColumn(field)); - } - - private interpretColumn (field: FunctionApplicationNode): CompileError[] { - const errors: CompileError[] = []; - - const column: Partial = {}; - - column.name = extractVarNameFromPrimaryVariable(field.callee as any).unwrap(); - - const typeReport = processColumnType(field.args[0], this.env); - column.type = typeReport.getValue(); - errors.push(...typeReport.getErrors()); - - column.token = getTokenPosition(field); - column.inline_refs = []; - - const settings = field.args.slice(1); - if (last(settings) instanceof ListExpressionNode) { - const settingMap = aggregateSettingList(settings.pop() as ListExpressionNode).getValue(); - - column.pk = !!settingMap[SettingName.PK]?.length || !!settingMap[SettingName.PrimaryKey]?.length; - column.increment = !!settingMap[SettingName.Increment]?.length; - column.unique = !!settingMap[SettingName.Unique]?.length; - - column.not_null = settingMap[SettingName.NotNull]?.length - ? true - : ( - settingMap[SettingName.Null]?.length - ? false - : undefined - ); - column.dbdefault = processDefaultValue(settingMap[SettingName.Default]?.at(0)?.value); - - const noteNode = settingMap[SettingName.Note]?.at(0); - column.note = noteNode && { - value: extractQuotedStringToken(noteNode.value).map(normalizeNoteContent).unwrap(), - token: getTokenPosition(noteNode), - }; - - const refs = settingMap[SettingName.Ref] || []; - column.inline_refs = refs.flatMap((ref) => { - const [referredSymbol] = getColumnSymbolsOfRefOperand((ref.value as PrefixExpressionNode).expression!); - - if (isSameEndpoint(referredSymbol, field.symbol as ColumnSymbol)) { - errors.push(new CompileError(CompileErrorCode.SAME_ENDPOINT, 'Two endpoints are the same', ref)); - return []; - } - - const op = (ref.value as PrefixExpressionNode).op!; - const fragments = destructureComplexVariable((ref.value as PrefixExpressionNode).expression).unwrap(); - - let inlineRef: InlineRef | undefined; - if (fragments.length === 2) { - const [table, columnName] = fragments; - inlineRef = { - schemaName: null, - tableName: table, - fieldNames: [columnName], - relation: op.value as any, - token: getTokenPosition(ref), - }; - } else if (fragments.length === 3) { - const [schema, table, columnName] = fragments; - inlineRef = { - schemaName: schema, - tableName: table, - fieldNames: [columnName], - relation: op.value as any, - token: getTokenPosition(ref), - }; - } else { - errors.push(new CompileError(CompileErrorCode.UNSUPPORTED, 'Unsupported', ref)); - const columnName = fragments.pop()!; - const table = fragments.pop()!; - const schema = fragments.join('.'); - inlineRef = { - schemaName: schema, - tableName: table, - fieldNames: [columnName], - relation: op.value as any, - token: getTokenPosition(ref), - }; - } - - return inlineRef; - }); - - const checkNodes = settingMap[SettingName.Check] || []; - column.checks = checkNodes.map((checkNode) => { - const token = getTokenPosition(checkNode); - const expression = (checkNode.value as FunctionExpressionNode).value!.value!; - return { - token, - expression, - }; - }); - } - - column.pk ||= settings.some((setting) => extractVariableFromExpression(setting).unwrap().toLowerCase() === 'pk'); - column.unique ||= settings.some((setting) => extractVariableFromExpression(setting).unwrap().toLowerCase() === 'unique'); - - this.tablePartial.fields!.push(column as Column); - if (column.pk) { - this.pkColumns.push(column as Column); - } - return errors; - } - - private interpretIndexes (indexes: ElementDeclarationNode): CompileError[] { - this.tablePartial.indexes!.push(...(indexes.body as BlockExpressionNode).body.map((_indexField) => { - const index: Partial = { columns: [] }; - - const indexField = _indexField as FunctionApplicationNode; - index.token = getTokenPosition(indexField); - const args = [indexField.callee!, ...indexField.args]; - if (last(args) instanceof ListExpressionNode) { - const settingMap = aggregateSettingList(args.pop() as ListExpressionNode).getValue(); - index.pk = !!settingMap[SettingName.PK]?.length; - index.unique = !!settingMap[SettingName.Unique]?.length; - index.name = extractQuotedStringToken(settingMap[SettingName.Name]?.at(0)?.value).unwrap_or(undefined); - const noteNode = settingMap[SettingName.Note]?.at(0); - index.note = noteNode && { - value: extractQuotedStringToken(noteNode.value).unwrap(), - token: getTokenPosition(noteNode), - }; - index.type = extractVariableFromExpression(settingMap[SettingName.Type]?.at(0)?.value).unwrap_or(undefined); - } - - args.flatMap((arg) => { - // This is to deal with indexes fields such as - // (id, name) (age, weight) - // which is a call expression - if (!(arg instanceof CallExpressionNode)) return arg; - - const fragments: SyntaxNode[] = []; - let argPtr = arg; - - while (argPtr instanceof CallExpressionNode) { - fragments.push(argPtr.argumentList!); - argPtr = argPtr.callee!; - } - fragments.push(argPtr); - return fragments; - }).forEach((arg) => { - const { functional, nonFunctional } = destructureIndexNode(arg).unwrap(); - index.columns!.push( - ...functional.map((s) => ({ - value: s.value!.value, - type: 'expression', - token: getTokenPosition(s), - })), - ...nonFunctional.map((s) => ({ - value: extractVarNameFromPrimaryVariable(s).unwrap(), - type: 'column', - token: getTokenPosition(s), - })), - ); - }); - - return index as Index; - })); - - return []; - } - - private interpretChecks (checks: ElementDeclarationNode): CompileError[] { - this.tablePartial.checks!.push(...(checks.body as BlockExpressionNode).body.map((_checkField) => { - const check: Partial = {}; - const checkField = _checkField as FunctionApplicationNode; - check.token = getTokenPosition(checkField); - - if (checkField.args[0] instanceof ListExpressionNode) { - const settingMap = aggregateSettingList(checkField.args[0] as ListExpressionNode).getValue(); - check.name = extractQuotedStringToken(settingMap[SettingName.Name]?.at(0)?.value).unwrap_or(undefined); - } - - check.expression = (checkField.callee as FunctionExpressionNode).value!.value!; - - return check as Check; - })); - - return []; - } -} diff --git a/packages/dbml-parse/src/core/interpreter/interpreter.ts b/packages/dbml-parse/src/core/interpreter/interpreter.ts deleted file mode 100644 index 9978449e8..000000000 --- a/packages/dbml-parse/src/core/interpreter/interpreter.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { ProgramNode } from '@/core/parser/nodes'; -import { Database, InterpreterDatabase, Table, TablePartial, TableRecord } from '@/core/interpreter/types'; -import { TableInterpreter } from '@/core/interpreter/elementInterpreter/table'; -import { StickyNoteInterpreter } from '@/core/interpreter/elementInterpreter/sticky_note'; -import { RefInterpreter } from '@/core/interpreter/elementInterpreter/ref'; -import { TableGroupInterpreter } from '@/core/interpreter/elementInterpreter/tableGroup'; -import { EnumInterpreter } from '@/core/interpreter/elementInterpreter/enum'; -import { ProjectInterpreter } from '@/core/interpreter/elementInterpreter/project'; -import { TablePartialInterpreter } from '@/core/interpreter/elementInterpreter/tablePartial'; -import { RecordsInterpreter } from '@/core/interpreter/records'; -import Report from '@/core/report'; -import { getElementKind } from '@/core/analyzer/utils'; -import { ElementKind } from '@/core/analyzer/types'; -import { CompileWarning } from '../errors'; -import { getTokenPosition } from './utils'; - -function processColumnInDb (table: T): T { - return { - ...table, - fields: table.fields.map((c) => ({ - ...c, - type: { - ...c.type, - isEnum: undefined, - lengthParam: undefined, - numericParams: undefined, - }, - })), - }; -} - -function convertEnvToDb (env: InterpreterDatabase): Database { - // Convert records Map to array of TableRecord - const records: TableRecord[] = []; - for (const [table, { element, rows }] of env.records) { - if (!rows.length) continue; - const columns = Object.keys(rows[0].columnNodes); - records.push({ - schemaName: table.schemaName || undefined, - tableName: table.name, - columns, - token: getTokenPosition(element), - values: rows.map((r) => { - // Convert object-based values to array-based values ordered by columns - return columns.map((col) => { - const val = r.values[col]; - if (val) { - return { value: val.value, type: val.type }; - } - return { value: null, type: 'expression' }; - }); - }), - }); - } - - return { - schemas: [], - tables: Array.from(env.tables.values()).map(processColumnInDb), - notes: Array.from(env.notes.values()), - refs: Array.from(env.ref.values()), - enums: Array.from(env.enums.values()), - tableGroups: Array.from(env.tableGroups.values()), - aliases: env.aliases, - project: Array.from(env.project.values())[0] || {}, - tablePartials: Array.from(env.tablePartials.values()).map(processColumnInDb), - records, - }; -} - -// The interpreted format follows the old parser -export default class Interpreter { - ast: ProgramNode; - env: InterpreterDatabase; - - constructor (ast: ProgramNode) { - this.ast = ast; - this.env = { - schema: [], - tables: new Map(), - notes: new Map(), - refIds: { }, - ref: new Map(), - enums: new Map(), - tableOwnerGroup: { }, - tableGroups: new Map(), - aliases: [], - project: new Map(), - tablePartials: new Map(), - records: new Map(), - recordsElements: [], - cachedMergedTables: new Map(), - source: ast.source, - }; - } - - interpret (): Report { - // First pass: interpret all non-records elements - const errors = this.ast.body.flatMap((element) => { - switch (getElementKind(element).unwrap_or(undefined)) { - case ElementKind.Table: - return (new TableInterpreter(element, this.env)).interpret(); - case ElementKind.Note: - return (new StickyNoteInterpreter(element, this.env)).interpret(); - case ElementKind.Ref: - return (new RefInterpreter(element, this.env)).interpret(); - case ElementKind.TableGroup: - return (new TableGroupInterpreter(element, this.env)).interpret(); - case ElementKind.TablePartial: - return (new TablePartialInterpreter(element, this.env)).interpret(); - case ElementKind.Enum: - return (new EnumInterpreter(element, this.env)).interpret(); - case ElementKind.Project: - return (new ProjectInterpreter(element, this.env)).interpret(); - case ElementKind.Records: - // Defer records interpretation - collect for later - this.env.recordsElements.push(element); - return []; - default: - return []; - } - }); - - const warnings: CompileWarning[] = []; - if (this.env.recordsElements.length) { - // Second pass: interpret all records elements grouped by table - // Now that all tables, enums, etc. are interpreted, we can validate records properly - const recordsResult = new RecordsInterpreter(this.env).interpret(this.env.recordsElements); - errors.push(...recordsResult.getErrors()); - warnings.push(...recordsResult.getWarnings()); - } - - return new Report(convertEnvToDb(this.env), errors, warnings); - } -} diff --git a/packages/dbml-parse/src/core/interpreter/records/index.ts b/packages/dbml-parse/src/core/interpreter/records/index.ts deleted file mode 100644 index 8c41d1670..000000000 --- a/packages/dbml-parse/src/core/interpreter/records/index.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { - BlockExpressionNode, - CommaExpressionNode, - ElementDeclarationNode, - FunctionApplicationNode, - FunctionExpressionNode, - SyntaxNode, - TupleExpressionNode, -} from '@/core/parser/nodes'; -import { CompileError, CompileErrorCode, CompileWarning } from '@/core/errors'; -import Report from '@/core/report'; -import { - RecordValue, - InterpreterDatabase, - Table, - Column, -} from '@/core/interpreter/types'; -import { - isNullish, - isEmptyStringLiteral, - tryExtractNumeric, - tryExtractBoolean, - tryExtractString, - tryExtractDateTime, - isNumericType, - isIntegerType, - isFloatType, - isBooleanType, - isStringType, - isDateTimeType, - getRecordValueType, - validatePrimaryKey, - validateUnique, - validateForeignKeys, - isSerialType, -} from './utils'; -import { destructureCallExpression, destructureComplexVariable, extractQuotedStringToken, extractVariableFromExpression } from '@/core/analyzer/utils'; -import { last } from 'lodash-es'; -import { mergeTableAndPartials } from '../utils'; - -export class RecordsInterpreter { - private env: InterpreterDatabase; - private tableToRecordMap: Map; - - constructor (env: InterpreterDatabase) { - this.env = env; - this.tableToRecordMap = new Map(); - } - - interpret (elements: ElementDeclarationNode[]): Report { - const errors: CompileError[] = []; - const warnings: CompileWarning[] = []; - - for (const element of elements) { - const { table, mergedColumns } = getTableAndColumnsOfRecords(element, this.env); - const prevRecord = this.tableToRecordMap.get(table); - if (prevRecord) { - errors.push(new CompileError( - CompileErrorCode.DUPLICATE_RECORDS_FOR_TABLE, - `Duplicate Records blocks for the same Table '${table.name}' - A Table can only have one Records block`, - prevRecord, - )); - errors.push(new CompileError( - CompileErrorCode.DUPLICATE_RECORDS_FOR_TABLE, - `Duplicate Records blocks for the same Table '${table.name}' - A Table can only have one Records block`, - element, - )); - continue; - } - this.tableToRecordMap.set(table, element); - if (!this.env.records.has(table)) { - this.env.records.set(table, { element, rows: [] }); - } - const tableRecords = this.env.records.get(table)!; - for (const row of (element.body as BlockExpressionNode).body) { - const rowNode = row as FunctionApplicationNode; - const result = extractDataFromRow(rowNode, mergedColumns, this.env); - errors.push(...result.getErrors()); - warnings.push(...result.getWarnings()); - const rowData = result.getValue(); - if (!rowData.row) continue; - tableRecords.rows.push({ - values: rowData.row, - node: rowNode, - columnNodes: rowData.columnNodes, - }); - } - } - - const constraintResult = this.validateConstraints(); - warnings.push(...constraintResult); - - return new Report(undefined, errors, warnings); - } - - private validateConstraints (): CompileWarning[] { - const warnings: CompileWarning[] = []; - - // Validate PK constraints - warnings.push(...validatePrimaryKey(this.env).map((e) => e.toWarning())); - - // Validate unique constraints - warnings.push(...validateUnique(this.env).map((e) => e.toWarning())); - - // Validate FK constraints - warnings.push(...validateForeignKeys(this.env).map((e) => e.toWarning())); - - return warnings; - } -} - -// Returns: -// - `table`: The original interpreted table object that `records` refer to -// - `mergedTable`: The interpreted table object merged with its table partials -// - `mergedColumns`: The columns of the `mergedTable`` -function getTableAndColumnsOfRecords (records: ElementDeclarationNode, env: InterpreterDatabase): { table: Table; mergedTable: Table; mergedColumns: Column[] } { - const nameNode = records.name; - const parent = records.parent; - if (parent instanceof ElementDeclarationNode) { - const table = env.tables.get(parent)!; - const mergedTable = mergeTableAndPartials(table, env); - if (!nameNode) return { - table, - mergedTable, - mergedColumns: mergedTable.fields, - }; - const mergedColumns = (nameNode as TupleExpressionNode).elementList.map((e) => mergedTable.fields.find((f) => f.name === extractVariableFromExpression(e).unwrap())!); - return { - table, - mergedTable, - mergedColumns, - }; - } - const fragments = destructureCallExpression(nameNode!).unwrap(); - const tableNode = last(fragments.variables)!.referee!.declaration as ElementDeclarationNode; - const table = env.tables.get(tableNode)!; - const mergedTable = mergeTableAndPartials(table, env); - const mergedColumns = fragments.args.map((e) => mergedTable.fields.find((f) => f.name === extractVariableFromExpression(e).unwrap())!); - return { - table, - mergedTable, - mergedColumns, - }; -} - -type RowData = { row: Record | null; columnNodes: Record }; - -function extractDataFromRow ( - row: FunctionApplicationNode, - mergedColumns: Column[], - env: InterpreterDatabase, -): Report { - const errors: CompileError[] = []; - const warnings: CompileWarning[] = []; - const rowObj: Record = {}; - const columnNodes: Record = {}; - - const args = row.callee instanceof CommaExpressionNode ? row.callee.elementList : [row.callee!]; - if (args.length !== mergedColumns.length) { - errors.push(new CompileError( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Expected ${mergedColumns.length} values but got ${args.length}`, - row, - )); - return new Report({ row: null, columnNodes: {} }, errors, warnings); - } - - for (let i = 0; i < mergedColumns.length; i++) { - const arg = args[i]; - const column = mergedColumns[i]; - columnNodes[column.name] = arg; - const result = extractValue(arg, column, env); - errors.push(...result.getErrors()); - warnings.push(...result.getWarnings()); - const value = result.getValue(); - if (value !== null) { - rowObj[column.name] = value; - } - } - - return new Report({ row: rowObj, columnNodes }, errors, warnings); -} - -function getNodeSourceText (node: SyntaxNode, source: string): string { - if (node instanceof FunctionExpressionNode) { - return node.value?.value || ''; - } - // Extract the source text using node start and end positions - if (!isNaN(node.start) && !isNaN(node.end)) { - return source.slice(node.start, node.end); - } - return ''; -} - -function extractValue ( - node: SyntaxNode, - column: Column, - env: InterpreterDatabase, -): Report { - // FIXME: Make this more precise - const type = column.type.type_name.split('(')[0]; - const { increment, not_null: notNull, dbdefault } = column; - const isEnum = column.type.isEnum || false; - const valueType = getRecordValueType(type, isEnum); - const rawString = tryExtractString(node); - const fallbackValue = rawString !== null ? rawString : getNodeSourceText(node, env.source); - const fallbackType = rawString !== null ? valueType : 'expression'; - - if (node instanceof FunctionExpressionNode) { - return new Report({ - value: node.value?.value || '', - type: 'expression', - }, [], []); - } - - // NULL literal - if (isNullish(node) || (isEmptyStringLiteral(node) && !isStringType(type))) { - const hasDefaultValue = dbdefault && dbdefault.value.toString().toLowerCase() !== 'null'; - const isSerial = isSerialType(type); - if (notNull && !hasDefaultValue && !increment && !isSerial) { - return new Report({ value: null, type: valueType }, [], [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `NULL not allowed for non-nullable column '${column.name}' without default and increment`, - node, - )]); - } - return new Report({ value: null, type: valueType }, [], []); - } - - // Enum type - if (isEnum) { - const enumMembers = ([...env.enums.values()].find((e) => e.schemaName === column.type.schemaName && e.name === column.type.type_name)?.values || []).map((field) => field.name); - let enumValue = extractQuotedStringToken(node).unwrap_or(undefined); - if (enumValue === undefined) { - enumValue = destructureComplexVariable(node).unwrap_or([]).pop(); - } - if (!(enumMembers as (string | undefined)[]).includes(enumValue)) { - return new Report({ value: enumValue, type: valueType }, [], [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Invalid enum value for column '${column.name}'`, - node, - )]); - } - - return new Report({ value: enumValue, type: valueType }, [], []); - } - - // Numeric type - if (isNumericType(type)) { - const numValue = tryExtractNumeric(node); - if (numValue === null) { - return new Report( - { value: fallbackValue, type: fallbackType }, - [], - [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Invalid numeric value for column '${column.name}'`, - node, - )], - ); - } - - // Integer type: validate no decimal point - if (isIntegerType(type) && !Number.isInteger(numValue)) { - return new Report({ value: Math.floor(numValue), type: valueType }, [], [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Invalid integer value ${numValue} for column '${column.name}': expected integer, got decimal`, - node, - )]); - } - - // Decimal/numeric type: validate precision and scale - if (isFloatType(type) && column.type.numericParams) { - const { precision, scale } = column.type.numericParams; - const numStr = numValue.toString(); - const parts = numStr.split('.'); - const integerPart = parts[0].replace(/^-/, ''); // Remove sign - const decimalPart = parts[1] || ''; - - const totalDigits = integerPart.length + decimalPart.length; - const decimalDigits = decimalPart.length; - - if (totalDigits > precision) { - return new Report({ value: numValue, type: valueType }, [], [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Numeric value ${numValue} for column '${column.name}' exceeds precision: expected at most ${precision} total digits, got ${totalDigits}`, - node, - )]); - } - - if (decimalDigits > scale) { - return new Report({ value: numValue, type: valueType }, [], [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Numeric value ${numValue} for column '${column.name}' exceeds scale: expected at most ${scale} decimal digits, got ${decimalDigits}`, - node, - )]); - } - } - - return new Report({ value: numValue, type: valueType }, [], []); - } - - // Boolean type - if (isBooleanType(type)) { - const boolValue = tryExtractBoolean(node); - if (boolValue === null) { - return new Report( - { value: fallbackValue, type: fallbackType }, - [], - [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Invalid boolean value for column '${column.name}'`, - node, - )], - ); - } - return new Report({ value: boolValue, type: valueType }, [], []); - } - - // Datetime type - if (isDateTimeType(type)) { - const dtValue = tryExtractDateTime(node); - if (dtValue === null) { - return new Report( - { value: fallbackValue, type: fallbackType }, - [], - [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Invalid datetime value for column '${column.name}', expected valid datetime format (e.g., 'YYYY-MM-DD', 'HH:MM:SS', 'YYYY-MM-DD HH:MM:SS', 'MM/DD/YYYY', 'D MMM YYYY', or 'MMM D, YYYY')`, - node, - )], - ); - } - return new Report({ value: dtValue, type: valueType }, [], []); - } - - // String type - if (isStringType(type)) { - const strValue = tryExtractString(node); - if (strValue === null) { - return new Report( - { value: fallbackValue, type: fallbackType }, - [], - [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `Invalid string value for column '${column.name}'`, - node, - )], - ); - } - - // Validate string length (using UTF-8 byte length like SQL engines) - if (column.type.lengthParam) { - const { length } = column.type.lengthParam; - // Calculate byte length in UTF-8 encoding (matching SQL behavior) - const actualByteLength = new TextEncoder().encode(strValue).length; - - if (actualByteLength > length) { - return new Report({ value: strValue, type: valueType }, [], [new CompileWarning( - CompileErrorCode.INVALID_RECORDS_FIELD, - `String value for column '${column.name}' exceeds maximum length: expected at most ${length} bytes (UTF-8), got ${actualByteLength} bytes`, - node, - )]); - } - } - - return new Report({ value: strValue, type: 'string' }, [], []); - } - - // Fallback - try to extract as string - return new Report({ value: fallbackValue, type: fallbackType }, [], []); -} diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/fk.ts b/packages/dbml-parse/src/core/interpreter/records/utils/constraints/fk.ts deleted file mode 100644 index 4f87ff2dd..000000000 --- a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/fk.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { CompileError } from '@/core/errors'; -import { InterpreterDatabase, Ref, RefEndpoint, Table, TableRecordRow } from '@/core/interpreter/types'; -import { - extractKeyValueWithDefault, - hasNullWithoutDefaultInKey, - formatFullColumnNames, - formatValues, - createConstraintErrors, -} from './helper'; -import { DEFAULT_SCHEMA_NAME } from '@/constants'; -import { mergeTableAndPartials, extractInlineRefsFromTablePartials } from '@/core/interpreter/utils'; -import { isEmpty, flatMap } from 'lodash-es'; - -type TableInfo = { - rows: TableRecordRow[]; - mergedTable: Table; -}; - -export function validateForeignKeys (env: InterpreterDatabase): CompileError[] { - // Collect all refs: explicit refs + inline refs from table partials - const refs = [ - ...env.ref.values(), - ...flatMap(Array.from(env.tables.values()), (t) => extractInlineRefsFromTablePartials(t, env)), - ]; - - // Build table info map - const tableInfoMap = buildTableInfoMap(env); - - return flatMap(refs, (ref) => validateRef(ref, tableInfoMap)); -} - -function buildTableInfoMap (env: InterpreterDatabase): Map { - const tableInfoMap = new Map(); - - for (const table of env.tables.values()) { - const key = makeTableKey(table.schemaName, table.name); - const rows = env.records.get(table)?.rows || []; - - if (!env.cachedMergedTables.has(table)) { - env.cachedMergedTables.set(table, mergeTableAndPartials(table, env)); - } - const mergedTable = env.cachedMergedTables.get(table)!; - - tableInfoMap.set(key, { mergedTable, rows }); - } - - return tableInfoMap; -} - -function makeTableKey (schema: string | null | undefined, table: string): string { - return schema ? `${schema}.${table}` : `${DEFAULT_SCHEMA_NAME}.${table}`; -} - -// Validate that source's values exist in target's values -function validateFkSourceToTarget ( - sourceTable: TableInfo, - targetTable: TableInfo, - sourceEndpoint: RefEndpoint, - targetEndpoint: RefEndpoint, -): CompileError[] { - if (isEmpty(sourceTable.rows)) return []; - - // Build set of valid target values for FK reference check - const validFkValues = new Set( - targetTable.rows.map((row) => extractKeyValueWithDefault(row.values, targetEndpoint.fieldNames)), - ); - - // Filter rows with NULL values (optional relationships) - const rowsWithValues = sourceTable.rows.filter((row) => - !hasNullWithoutDefaultInKey(row.values, sourceEndpoint.fieldNames), - ); - - // Find rows with FK values that don't exist in target - const invalidRows = rowsWithValues.filter((row) => { - const fkValue = extractKeyValueWithDefault(row.values, sourceEndpoint.fieldNames); - return !validFkValues.has(fkValue); - }); - - // Transform invalid rows to errors - return flatMap(invalidRows, (row) => { - const sourceColumnRef = formatFullColumnNames( - sourceTable.mergedTable.schemaName, - sourceTable.mergedTable.name, - sourceEndpoint.fieldNames, - ); - const targetColumnRef = formatFullColumnNames( - targetTable.mergedTable.schemaName, - targetTable.mergedTable.name, - targetEndpoint.fieldNames, - ); - const valueStr = formatValues(row.values, sourceEndpoint.fieldNames); - const message = `FK violation: ${sourceColumnRef} = ${valueStr} does not exist in ${targetColumnRef}`; - - return createConstraintErrors(row, sourceEndpoint.fieldNames, message); - }); -} - -function validateRef (ref: Ref, tableInfoMap: Map): CompileError[] { - if (!ref.endpoints) return []; - - const [endpoint1, endpoint2] = ref.endpoints; - const table1 = tableInfoMap.get(makeTableKey(endpoint1.schemaName, endpoint1.tableName)); - const table2 = tableInfoMap.get(makeTableKey(endpoint2.schemaName, endpoint2.tableName)); - - if (!table1 || !table2) return []; - - return validateRelationship(table1, table2, endpoint1, endpoint2); -} - -function validateRelationship ( - table1: TableInfo, - table2: TableInfo, - endpoint1: RefEndpoint, - endpoint2: RefEndpoint, -): CompileError[] { - const rel1 = endpoint1.relation; - const rel2 = endpoint2.relation; - - // Bidirectional relationships: both 1-1 and many-to-many - const isBidirectional = (rel1 === '1' && rel2 === '1') || (rel1 === '*' && rel2 === '*'); - if (isBidirectional) { - return [ - ...validateFkSourceToTarget(table1, table2, endpoint1, endpoint2), - ...validateFkSourceToTarget(table2, table1, endpoint2, endpoint1), - ]; - } - - // Many-to-one: validate FK from "many" side to "one" side - if (rel1 === '*' && rel2 === '1') { - return validateFkSourceToTarget(table1, table2, endpoint1, endpoint2); - } - - if (rel1 === '1' && rel2 === '*') { - return validateFkSourceToTarget(table2, table1, endpoint2, endpoint1); - } - - return []; -} diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/helper.ts b/packages/dbml-parse/src/core/interpreter/records/utils/constraints/helper.ts deleted file mode 100644 index 81c1c3e64..000000000 --- a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/helper.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { RecordValue, Column, TableRecordRow } from '@/core/interpreter/types'; -import { isSerialType } from '../data'; -import { CompileError, CompileErrorCode } from '@/core/errors'; - -export function extractKeyValueWithDefault ( - row: Record, - columnNames: string[], - columns?: (Column | undefined)[], -): string { - return columnNames.map((name, idx) => { - const value = row[name]?.value; - - if ((value === null || value === undefined) && columns && columns[idx]) { - const column = columns[idx]; - if (column?.dbdefault) { - return JSON.stringify(column.dbdefault.value); - } - } - - return JSON.stringify(value); - }).join('|'); -} - -export function hasNullWithoutDefaultInKey ( - row: Record, - columnNames: string[], - columns?: (Column | undefined)[], -): boolean { - return columnNames.some((name, idx) => { - const value = row[name]?.value; - - if ((value === null || value === undefined) && columns && columns[idx]) { - const column = columns[idx]; - if (column?.dbdefault) { - return false; - } - } - - return value === null || value === undefined; - }); -} - -export function isAutoIncrementColumn (column: Column): boolean { - return column.increment || isSerialType(column.type.type_name); -} - -export function hasNotNullWithDefault (column: Column): boolean { - return (column.not_null || false) && !!column.dbdefault; -} - -export function formatFullColumnName ( - schemaName: string | null, - tableName: string, - columnName: string, -): string { - if (schemaName) { - return `${schemaName}.${tableName}.${columnName}`; - } - return `${tableName}.${columnName}`; -} - -export function formatFullColumnNames ( - schemaName: string | null, - tableName: string, - columnNames: string[], -): string { - if (columnNames.length === 1) { - return formatFullColumnName(schemaName, tableName, columnNames[0]); - } - const formatted = columnNames.map((col) => formatFullColumnName(schemaName, tableName, col)); - return `(${formatted.join(', ')})`; -} - -// Format values to put in error messages -// e.g. 1 -> '1' -// e.g. 'a' -> '"a"' -// e.g. 1, 'a' -> '(1, "a")' -export function formatValues ( - row: Record, - columnNames: string[], -): string { - if (columnNames.length === 1) { - return JSON.stringify(row[columnNames[0]]?.value); - } - const values = columnNames.map((col) => JSON.stringify(row[col]?.value)).join(', '); - return `(${values})`; -} - -// For a row and a set of columns -// Add one compile error for each cell in the row corresponding to each column in the set -export function createConstraintErrors ( - row: TableRecordRow, - columnNames: string[], - message: string, -): CompileError[] { - const errorNodes = columnNames - .map((col) => row.columnNodes[col]) - .filter(Boolean); - - if (errorNodes.length > 0) { - return errorNodes.map((node) => new CompileError( - CompileErrorCode.INVALID_RECORDS_FIELD, - message, - node, - )); - } - - return [new CompileError( - CompileErrorCode.INVALID_RECORDS_FIELD, - message, - row.node, - )]; -} diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/pk.ts b/packages/dbml-parse/src/core/interpreter/records/utils/constraints/pk.ts deleted file mode 100644 index 5a1470804..000000000 --- a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/pk.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { InterpreterDatabase, Table, Column, TableRecordRow } from '@/core/interpreter/types'; -import { - extractKeyValueWithDefault, - hasNullWithoutDefaultInKey, - isAutoIncrementColumn, - formatFullColumnNames, - formatValues, - createConstraintErrors, -} from './helper'; -import { mergeTableAndPartials } from '@/core/interpreter/utils'; -import { isSerialType } from '../data'; -import { keyBy, groupBy, partition, compact, isEmpty, difference, filter, flatMap } from 'lodash-es'; - -const getConstraintType = (columnCount: number) => - columnCount > 1 ? 'Composite PK' : 'PK'; - -export function validatePrimaryKey (env: InterpreterDatabase): CompileError[] { - return flatMap(Array.from(env.records), ([table, { rows }]) => { - if (isEmpty(rows)) return []; - - if (!env.cachedMergedTables.has(table)) { - env.cachedMergedTables.set(table, mergeTableAndPartials(table, env)); - } - const mergedTable = env.cachedMergedTables.get(table)!; - - const pkConstraints = collectPkConstraints(mergedTable); - const availableColumns = collectAvailableColumns(rows); - const columnMap = keyBy(mergedTable.fields, 'name'); - - return flatMap(pkConstraints, (pkColumns) => - validatePkConstraint(pkColumns, rows, availableColumns, columnMap, mergedTable), - ); - }); -} - -function validatePkConstraint ( - pkColumns: string[], - rows: TableRecordRow[], - availableColumns: Set, - columnMap: Record, - mergedTable: Table, -): CompileError[] { - // Check for missing columns - const missingErrors = checkMissingPkColumns( - pkColumns, - availableColumns, - columnMap, - mergedTable, - rows, - ); - if (!isEmpty(missingErrors)) return missingErrors; - - // Get column definitions - const pkColumnFields = compact(pkColumns.map((col) => columnMap[col])); - const areAllColumnsAutoIncrement = pkColumnFields.every((col) => - col && isAutoIncrementColumn(col), - ); - - // Partition rows into those with NULL and those without - const [rowsWithNull, rowsWithoutNull] = partition(rows, (row) => - hasNullWithoutDefaultInKey(row.values, pkColumns, pkColumnFields), - ); - - // Validate NULL rows (only error if not all columns are auto-increment) - const nullErrors = areAllColumnsAutoIncrement - ? [] - : createNullErrors(rowsWithNull, pkColumns, mergedTable); - - // Find duplicate rows using groupBy - const duplicateErrors = findDuplicateErrors( - rowsWithoutNull, - pkColumns, - pkColumnFields, - mergedTable, - ); - - return [...nullErrors, ...duplicateErrors]; -} - -function createNullErrors ( - rowsWithNull: TableRecordRow[], - pkColumns: string[], - mergedTable: Table, -): CompileError[] { - if (isEmpty(rowsWithNull)) return []; - - const constraintType = getConstraintType(pkColumns.length); - const columnRef = formatFullColumnNames( - mergedTable.schemaName, - mergedTable.name, - pkColumns, - ); - const message = `NULL in ${constraintType}: ${columnRef} cannot be NULL`; - - return flatMap(rowsWithNull, (row) => - createConstraintErrors(row, pkColumns, message), - ); -} - -function findDuplicateErrors ( - rows: TableRecordRow[], - pkColumns: string[], - pkColumnFields: Column[], - mergedTable: Table, -): CompileError[] { - // Group rows by their PK value - const rowsByKeyValue = groupBy(rows, (row) => - extractKeyValueWithDefault(row.values, pkColumns, pkColumnFields), - ); - - // Find groups with more than 1 row (duplicates) - const duplicateGroups = filter(rowsByKeyValue, (group) => group.length > 1); - - // Transform duplicate groups to errors - return flatMap(duplicateGroups, (duplicateRows) => { - const constraintType = getConstraintType(pkColumns.length); - const columnRef = formatFullColumnNames( - mergedTable.schemaName, - mergedTable.name, - pkColumns, - ); - - // Skip first occurrence, report rest as duplicates - return flatMap(duplicateRows.slice(1), (row) => { - const valueStr = formatValues(row.values, pkColumns); - const message = `Duplicate ${constraintType}: ${columnRef} = ${valueStr}`; - return createConstraintErrors(row, pkColumns, message); - }); - }); -} - -function collectPkConstraints (mergedTable: Table): string[][] { - return [ - ...mergedTable.fields.filter((field) => field.pk).map((field) => [field.name]), - ...mergedTable.indexes.filter((index) => index.pk).map((index) => index.columns.map((c) => c.value)), - ]; -} - -function collectAvailableColumns (rows: TableRecordRow[]): Set { - return new Set(rows.flatMap((row) => Object.keys(row.values))); -} - -function checkMissingPkColumns ( - pkColumns: string[], - availableColumns: Set, - columnMap: Record, - mergedTable: Table, - rows: TableRecordRow[], -): CompileError[] { - // Use difference to find missing columns - const missingColumns = difference(pkColumns, Array.from(availableColumns)); - if (isEmpty(missingColumns)) return []; - - // Filter to only those without defaults - const hasNoDefaultValue = (colName: string): boolean => { - const col = columnMap[colName]; - return col && !col.increment && !isSerialType(col.type.type_name) && !col.dbdefault; - }; - - const missingWithoutDefaults = missingColumns.filter(hasNoDefaultValue); - if (isEmpty(missingWithoutDefaults)) return []; - - const constraintType = getConstraintType(missingWithoutDefaults.length); - const columnRef = formatFullColumnNames( - mergedTable.schemaName, - mergedTable.name, - missingWithoutDefaults, - ); - const message = `${constraintType}: Column ${columnRef} is missing from record and has no default value`; - - return rows.map((row) => new CompileError( - CompileErrorCode.INVALID_RECORDS_FIELD, - message, - row.node, - )); -} diff --git a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/unique.ts b/packages/dbml-parse/src/core/interpreter/records/utils/constraints/unique.ts deleted file mode 100644 index 16b901cc2..000000000 --- a/packages/dbml-parse/src/core/interpreter/records/utils/constraints/unique.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { CompileError } from '@/core/errors'; -import { InterpreterDatabase, Table, Column, TableRecordRow } from '@/core/interpreter/types'; -import { - extractKeyValueWithDefault, - hasNullWithoutDefaultInKey, - formatFullColumnNames, - formatValues, - createConstraintErrors, -} from './helper'; -import { mergeTableAndPartials } from '@/core/interpreter/utils'; -import { keyBy, groupBy, compact, isEmpty, filter, flatMap } from 'lodash-es'; - -const getConstraintType = (columnCount: number) => - columnCount > 1 ? 'Composite UNIQUE' : 'UNIQUE'; - -export function validateUnique (env: InterpreterDatabase): CompileError[] { - return flatMap(Array.from(env.records), ([table, { rows }]) => { - if (!env.cachedMergedTables.has(table)) { - env.cachedMergedTables.set(table, mergeTableAndPartials(table, env)); - } - const mergedTable = env.cachedMergedTables.get(table)!; - - if (isEmpty(rows)) return []; - - const uniqueConstraints = collectUniqueConstraints(mergedTable); - const columnMap = keyBy(mergedTable.fields, 'name'); - - return flatMap(uniqueConstraints, (uniqueColumns) => { - const uniqueColumnFields = compact(uniqueColumns.map((col) => columnMap[col])); - return checkUniqueDuplicates(rows, uniqueColumns, uniqueColumnFields, mergedTable); - }); - }); -} - -function collectUniqueConstraints (mergedTable: Table): string[][] { - return [ - ...mergedTable.fields.filter((field) => field.unique).map((field) => [field.name]), - ...mergedTable.indexes.filter((index) => index.unique).map((index) => index.columns.map((c) => c.value)), - ]; -} - -function checkUniqueDuplicates ( - rows: TableRecordRow[], - uniqueColumns: string[], - uniqueColumnFields: (Column | undefined)[], - mergedTable: Table, -): CompileError[] { - // Filter out rows with NULL values (SQL standard: NULLs don't conflict in UNIQUE constraints) - const rowsWithoutNull = rows.filter((row) => - !hasNullWithoutDefaultInKey(row.values, uniqueColumns, uniqueColumnFields), - ); - - // Group rows by their unique key value - const rowsByKeyValue = groupBy(rowsWithoutNull, (row) => - extractKeyValueWithDefault(row.values, uniqueColumns, uniqueColumnFields), - ); - - // Find groups with more than 1 row (duplicates) - const duplicateGroups = filter(rowsByKeyValue, (group) => group.length > 1); - - // Transform duplicate groups to errors - return flatMap(duplicateGroups, (duplicateRows) => { - const constraintType = getConstraintType(uniqueColumns.length); - const columnRef = formatFullColumnNames( - mergedTable.schemaName, - mergedTable.name, - uniqueColumns, - ); - - // Skip first occurrence, report rest as duplicates - return flatMap(duplicateRows.slice(1), (row) => { - const valueStr = formatValues(row.values, uniqueColumns); - const message = `Duplicate ${constraintType}: ${columnRef} = ${valueStr}`; - return createConstraintErrors(row, uniqueColumns, message); - }); - }); -} diff --git a/packages/dbml-parse/src/core/interpreter/types.ts b/packages/dbml-parse/src/core/interpreter/types.ts deleted file mode 100644 index e8f5f00a9..000000000 --- a/packages/dbml-parse/src/core/interpreter/types.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { ElementDeclarationNode, FunctionApplicationNode, SyntaxNode } from '@/core/parser/nodes'; -import { Position } from '@/core/types'; -import { CompileError } from '@/core/errors'; - -export interface TokenPosition { - start: Position; - end: Position; -} - -export interface ElementInterpreter { - interpret(): CompileError[]; -} - -export interface InterpreterDatabase { - schema: []; - tables: Map; - notes: Map; - // for keeping track of circular refs - refIds: { [refid: string]: ElementDeclarationNode }; - ref: Map; - enums: Map; - tableOwnerGroup: { [tableid: string]: ElementDeclarationNode }; - tableGroups: Map; - tablePartials: Map; - aliases: Alias[]; - project: Map; - records: Map; - recordsElements: ElementDeclarationNode[]; - cachedMergedTables: Map; // map Table to Table that has been merged with table partials - source: string; -} - -// Record value type -export type RecordValueType = 'string' | 'bool' | 'integer' | 'real' | 'date' | 'time' | 'datetime' | string; - -export interface RecordValue { - value: any; - type: RecordValueType; -} - -export interface TableRecordRow { - values: Record; - node: FunctionApplicationNode; - columnNodes: Record; // Map of column name to its value node -} - -export interface TableRecordsData { - table: Table; - rows: TableRecordRow[]; -} - -export interface TableRecord { - schemaName: string | undefined; - tableName: string; - columns: string[]; - values: RecordValue[][]; - token: TokenPosition; -} - -export interface Database { - schemas: []; - tables: Table[]; - notes: Note[]; - refs: Ref[]; - enums: Enum[]; - tableGroups: TableGroup[]; - aliases: Alias[]; - project: Project; - tablePartials: TablePartial[]; - records: TableRecord[]; -} - -export interface Table { - name: string; - schemaName: null | string; - alias: string | null; - fields: Column[]; // The order of fields must match the order of declaration - checks: Check[]; - partials: TablePartialInjection[]; - token: TokenPosition; - indexes: Index[]; - headerColor?: string; - note?: { - value: string; - token: TokenPosition; - }; -} - -export interface Note { - name: string; - content: string; - token: TokenPosition; - headerColor?: string; -} - -export interface ColumnType { - schemaName: string | null; - type_name: string; - args: string | null; - // Parsed type parameters - stripped when passed to @dbml/core - numericParams?: { precision: number; scale: number }; - lengthParam?: { length: number }; - // Whether this type references an enum - stripped when passed to @dbml/core - isEnum?: boolean; -} - -export interface Column { - name: string; - type: ColumnType; - token: TokenPosition; - inline_refs: InlineRef[]; - checks: Check[]; - pk?: boolean; - dbdefault?: { - type: 'number' | 'string' | 'boolean' | 'expression'; - value: number | string; - }; - increment?: boolean; - unique?: boolean; - not_null?: boolean; - note?: { - value: string; - token: TokenPosition; - }; -} - -export interface Index { - columns: { - value: string; - type: string; - token: TokenPosition; - }[]; - token: TokenPosition; - unique?: boolean; - pk?: boolean; - name?: string; - note?: { - value: string; - token: TokenPosition; - }; - type?: string; -} - -export interface Check { - token: TokenPosition; - expression: string; - name?: string; -} - -export interface InlineRef { - schemaName: string | null; - tableName: string; - fieldNames: string[]; - relation: '>' | '<' | '-' | '<>'; - token: TokenPosition; -} - -export interface Ref { - schemaName: string | null; - name: string | null; - endpoints: RefEndpointPair; - color?: string; - onDelete?: string; - onUpdate?: string; - token: TokenPosition; -} - -export type RefEndpointPair = [RefEndpoint, RefEndpoint]; - -export interface RefEndpoint { - schemaName: string | null; - tableName: string; - fieldNames: string[]; - relation: RelationCardinality; - token: TokenPosition; -} - -export type RelationCardinality = '1' | '*'; - -export interface Enum { - name: string; - schemaName: string | null; - token: TokenPosition; - values: EnumField[]; -} - -export interface EnumField { - name: string; - token: TokenPosition; - note?: { - value: string; - token: TokenPosition; - }; -} - -export interface TableGroup { - name: string | null; - schemaName: string | null; - tables: TableGroupField[]; - token: TokenPosition; - color?: string; - note?: { - value: string; - token: TokenPosition; - }; -} - -export interface TableGroupField { - name: string; - schemaName: string | null; -} - -export interface Alias { - name: string; - kind: 'table'; - value: { - tableName: string; - schemaName: string | null; - }; -} - -export interface TablePartial { - name: string; - fields: Column[]; - token: TokenPosition; - indexes: Index[]; - headerColor?: string; - checks: Check[]; - note?: { - value: string; - token: TokenPosition; - }; -} - -export interface TablePartialInjection { - name: string; - order: number; - token: TokenPosition; -} - -export type Project = - | Record - | { - name: string | null; - tables: Table[]; - refs: Ref[]; - enums: Enum[]; - tableGroups: TableGroup[]; - tablePartials: TablePartial[]; - note?: { - value: string; - token: TokenPosition; - }; - token: TokenPosition; - [ - index: string & Omit - ]: string; - }; diff --git a/packages/dbml-parse/src/core/interpreter/utils.ts b/packages/dbml-parse/src/core/interpreter/utils.ts deleted file mode 100644 index 023404e4c..000000000 --- a/packages/dbml-parse/src/core/interpreter/utils.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { last, zip, uniqBy } from 'lodash-es'; -import { ColumnSymbol } from '@/core/analyzer/symbol/symbols'; -import { - destructureComplexVariableTuple, destructureComplexVariable, destructureMemberAccessExpression, extractQuotedStringToken, - extractVariableFromExpression, - extractVarNameFromPrimaryVariable, -} from '@/core/analyzer/utils'; -import { - ArrayNode, BlockExpressionNode, CallExpressionNode, FunctionExpressionNode, FunctionApplicationNode, LiteralNode, - PrimaryExpressionNode, SyntaxNode, TupleExpressionNode, -} from '@/core/parser/nodes'; -import { - ColumnType, RelationCardinality, Table, TokenPosition, InterpreterDatabase, Ref, - Column, -} from '@/core/interpreter/types'; -import { SyntaxTokenKind } from '@/core/lexer/tokens'; -import { isDotDelimitedIdentifier, isExpressionAnIdentifierNode, isExpressionAQuotedString } from '@/core/parser/utils'; -import Report from '@/core/report'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { getNumberTextFromExpression, parseNumber } from '@/core/utils'; -import { isExpressionASignedNumberExpression, isValidPartialInjection } from '../analyzer/validator/utils'; - -export function extractNamesFromRefOperand (operand: SyntaxNode, owner?: Table): { schemaName: string | null; tableName: string; fieldNames: string[] } { - const { variables, tupleElements } = destructureComplexVariableTuple(operand).unwrap(); - - const tupleNames = tupleElements.map((e) => extractVarNameFromPrimaryVariable(e).unwrap()); - const variableNames = variables.map((e) => extractVarNameFromPrimaryVariable(e).unwrap()); - - if (tupleElements.length) { - if (variables.length === 0) { - return { - schemaName: owner!.schemaName, - tableName: owner!.name, - fieldNames: tupleNames, - }; - } - - return { - tableName: variableNames.pop()!, - schemaName: variableNames.pop() || null, - fieldNames: tupleNames, - }; - } - - if (variables.length === 1) { - return { - schemaName: owner!.schemaName, - tableName: owner!.name, - fieldNames: [variableNames[0]], - }; - } - - return { - fieldNames: [variableNames.pop()!], - tableName: variableNames.pop()!, - schemaName: variableNames.pop() || null, - }; -} - -export function getMultiplicities ( - op: string, -): [RelationCardinality, RelationCardinality] { - switch (op) { - case '<': - return ['1', '*']; - case '<>': - return ['*', '*']; - case '>': - return ['*', '1']; - case '-': - return ['1', '1']; - default: - throw new Error('Invalid relation op'); - } -} - -export function getTokenPosition (node: SyntaxNode): TokenPosition { - return { - start: { - offset: node.startPos.offset, - line: node.startPos.line + 1, - column: node.startPos.column + 1, - }, - end: { - offset: node.endPos.offset, - line: node.endPos.line + 1, - column: node.endPos.column + 1, - }, - }; -} - -export function getColumnSymbolsOfRefOperand (ref: SyntaxNode): ColumnSymbol[] { - const colNode = destructureMemberAccessExpression(ref).unwrap_or(undefined)?.pop(); - if (colNode instanceof TupleExpressionNode) { - return colNode.elementList.map((e) => e.referee as ColumnSymbol); - } - return [colNode!.referee as ColumnSymbol]; -} - -export function extractElementName (nameNode: SyntaxNode): { schemaName: string[]; name: string } { - const fragments = destructureComplexVariable(nameNode).unwrap(); - const name = fragments.pop()!; - - return { - name, - schemaName: fragments, - }; -} - -export function extractColor (node: PrimaryExpressionNode & { expression: LiteralNode } & { literal: { kind: SyntaxTokenKind.COLOR_LITERAL } }): string { - return node.expression.literal!.value; -} - -export function getRefId (sym1: ColumnSymbol, sym2: ColumnSymbol): string; -export function getRefId (sym1: ColumnSymbol[], sym2: ColumnSymbol[]): string; -export function getRefId (sym1: ColumnSymbol | ColumnSymbol[], sym2: ColumnSymbol | ColumnSymbol[]): string { - if (Array.isArray(sym1)) { - const firstIds = sym1.map(({ id }) => id).sort().join(','); - const secondIds = (sym2 as ColumnSymbol[]).map(({ id }) => id).sort().join(','); - return firstIds < secondIds ? `${firstIds}-${secondIds}` : `${secondIds}-${firstIds}`; - } - - const firstId = sym1.id.toString(); - const secondId = (sym2 as ColumnSymbol).id.toString(); - return firstId < secondId ? `${firstId}-${secondId}` : `${secondId}-${firstId}`; -} - -export function isSameEndpoint (sym1: ColumnSymbol, sym2: ColumnSymbol): boolean; -export function isSameEndpoint (sym1: ColumnSymbol[], sym2: ColumnSymbol[]): boolean; -export function isSameEndpoint (sym1: ColumnSymbol | ColumnSymbol[], sym2: ColumnSymbol | ColumnSymbol[]): boolean { - if (Array.isArray(sym1)) { - const firstIds = sym1.map(({ id }) => id).sort(); - const secondIds = (sym2 as ColumnSymbol[]).map(({ id }) => id).sort(); - return zip(firstIds, secondIds).every(([first, second]) => first === second); - } - - const firstId = sym1.id; - const secondId = (sym2 as ColumnSymbol).id; - return firstId === secondId; -} - -export function normalizeNoteContent (content: string): string { - const lines = content.split('\n'); - - // Top empty lines are trimmed - const trimmedTopEmptyLines = lines.slice(lines.findIndex((line) => line.trimStart() !== '')); - - // Calculate min-indentation, empty lines are ignored - const nonEmptyLines = trimmedTopEmptyLines.filter((line) => line.trimStart()); - const minIndent = Math.min(...nonEmptyLines.map((line) => line.length - line.trimStart().length)); - - return trimmedTopEmptyLines.map((line) => line.slice(minIndent)).join('\n'); -} - -export function processDefaultValue (valueNode?: SyntaxNode): - { - type: 'string' | 'number' | 'boolean' | 'expression'; - value: string | number; - } | undefined { - if (!valueNode) { - return undefined; - } - - if (isExpressionAQuotedString(valueNode)) { - return { - value: extractQuotedStringToken(valueNode).unwrap(), - type: 'string', - }; - } - - if (isExpressionASignedNumberExpression(valueNode)) { - return { - type: 'number', - value: parseNumber(valueNode), - }; - } - - if (isExpressionAnIdentifierNode(valueNode)) { - const value = valueNode.expression.variable.value.toLowerCase(); - return { - value, - type: 'boolean', - }; - } - - if (valueNode instanceof FunctionExpressionNode && valueNode.value) { - return { - value: valueNode.value.value, - type: 'expression', - }; - } - - if (isDotDelimitedIdentifier(valueNode)) { - return { - value: destructureMemberAccessExpression(valueNode).map(last).and_then(extractVariableFromExpression).unwrap(), - type: 'string', - }; - } - - throw new Error('Unreachable'); -} - -export function processColumnType (typeNode: SyntaxNode, env: InterpreterDatabase): Report { - let typeSuffix: string = ''; - let typeArgs: string | null = null; - let numericParams: { precision: number; scale: number } | undefined; - let lengthParam: { length: number } | undefined; - - if (typeNode instanceof CallExpressionNode) { - const argElements = typeNode.argumentList!.elementList; - typeArgs = argElements.map((e) => { - if (isExpressionASignedNumberExpression(e)) { - return getNumberTextFromExpression(e); - } - if (isExpressionAQuotedString(e)) { - return extractQuotedStringToken(e).unwrap(); - } - // e can only be an identifier here - return extractVariableFromExpression(e).unwrap(); - }).join(','); - typeSuffix = `(${typeArgs})`; - - // Parse numeric type parameters (precision, scale) - if (argElements.length === 2 - && isExpressionASignedNumberExpression(argElements[0]) - && isExpressionASignedNumberExpression(argElements[1])) { - const precision = parseNumber(argElements[0]); - const scale = parseNumber(argElements[1]); - if (!isNaN(precision) && !isNaN(scale)) { - numericParams = { precision: Math.trunc(precision), scale: Math.trunc(scale) }; - } - } else if (argElements.length === 1 && isExpressionASignedNumberExpression(argElements[0])) { - const length = parseNumber(argElements[0]); - if (!isNaN(length)) { - lengthParam = { length: Math.trunc(length) }; - } - } - - typeNode = typeNode.callee!; - } - while (typeNode instanceof CallExpressionNode || typeNode instanceof ArrayNode) { - if (typeNode instanceof CallExpressionNode) { - const args = typeNode - .argumentList!.elementList.map((e) => { - if (isExpressionASignedNumberExpression(e)) { - return getNumberTextFromExpression(e); - } - if (isExpressionAQuotedString(e)) { - return extractQuotedStringToken(e).unwrap(); - } - // e can only be an identifier here - return extractVariableFromExpression(e).unwrap(); - }) - .join(','); - typeSuffix = `(${args})${typeSuffix}`; - typeNode = typeNode.callee!; - } else if (typeNode instanceof ArrayNode) { - const indexer = `[${ - typeNode - .indexer!.elementList.map((e) => (e.name as any).expression.literal.value) - .join(',') - }]`; - typeSuffix = `${indexer}${typeSuffix}`; - typeNode = typeNode.array!; - } - } - - const { name: typeName, schemaName: typeSchemaName } = extractElementName(typeNode); - - // Check if this type references an enum - const schema = typeSchemaName.length === 0 ? null : typeSchemaName[0]; - - const isEnum = !![...env.enums.values()].find((e) => e.name === typeName && e.schemaName === schema); - - if (typeSchemaName.length > 1) { - return new Report( - { - schemaName: typeSchemaName.length === 0 ? null : typeSchemaName[0], - type_name: `${typeName}${typeSuffix}`, - args: typeArgs, - numericParams, - lengthParam, - isEnum, - }, - [new CompileError(CompileErrorCode.UNSUPPORTED, 'Nested schema is not supported', typeNode)], - ); - } - - return new Report({ - schemaName: typeSchemaName.length === 0 ? null : typeSchemaName[0], - type_name: `${typeName}${typeSuffix}`, - args: typeArgs, - numericParams, - lengthParam, - isEnum, - }); -} - -// The returned table respects (injected) column definition order -export function mergeTableAndPartials (table: Table, env: InterpreterDatabase): Table { - const tableElement = [...env.tables.entries()].find(([, t]) => t === table)?.[0]; - if (!tableElement) { - throw new Error('mergeTableAndPartials should be called after all tables are interpreted'); - } - if (!(tableElement.body instanceof BlockExpressionNode)) { - throw new Error('Table element should have a block body'); - } - - const indexes = [...table.indexes]; - const checks = [...table.checks]; - let headerColor = table.headerColor; - let note = table.note; - - const tablePartials = [...env.tablePartials.values()]; - // Prioritize later table partials - for (const tablePartial of [...table.partials].reverse()) { - const { name } = tablePartial; - const partial = tablePartials.find((p) => p.name === name); - if (!partial) continue; - - // Merge indexes - indexes.push(...partial.indexes); - - // Merge checks - checks.push(...partial.checks); - - // Merge settings (later partials override) - if (partial.headerColor !== undefined) { - headerColor = partial.headerColor; - } - if (partial.note !== undefined) { - note = partial.note; - } - } - - const directFieldMap = new Map(table.fields.map((f) => [f.name, f])); - const directFieldNames = new Set(directFieldMap.keys()); - const partialMap = new Map(tablePartials.map((p) => [p.name, p])); - - // Collect all fields in declaration order - const allFields: Column[] = []; - - for (const subfield of tableElement.body.body) { - if (!(subfield instanceof FunctionApplicationNode)) continue; - - if (isValidPartialInjection(subfield.callee)) { - // Inject partial fields - const partialName = extractVariableFromExpression(subfield.callee.expression).unwrap_or(undefined); - const partial = partialMap.get(partialName!); - if (!partial) continue; - - for (const field of partial.fields) { - // Skip if overridden by direct definition - if (directFieldNames.has(field.name)) continue; - allFields.push(field); - } - } else { - // Add direct field definition - const columnName = extractVariableFromExpression(subfield.callee).unwrap(); - const column = directFieldMap.get(columnName); - if (!column) continue; - allFields.push(column); - } - } - - // Use uniqBy to keep last occurrence of each field (later partials win) - // Process from end to start, then reverse to maintain declaration order - const fields = uniqBy([...allFields].reverse(), 'name').reverse(); - - return { - ...table, - fields, - indexes, - checks, - headerColor, - note, - }; -} - -export function extractInlineRefsFromTablePartials (table: Table, env: InterpreterDatabase): Ref[] { - const refs: Ref[] = []; - const tablePartials = [...env.tablePartials.values()]; - const originalFieldNames = new Set(table.fields.map((f) => f.name)); - - // Process partials in the same order as mergeTableAndPartials - for (const tablePartial of [...table.partials].reverse()) { - const { name } = tablePartial; - const partial = tablePartials.find((p) => p.name === name); - if (!partial) continue; - - // Extract inline refs from partial fields - for (const field of partial.fields) { - // Skip if this field is overridden by the original table - if (originalFieldNames.has(field.name)) continue; - - for (const inlineRef of field.inline_refs) { - const multiplicities = getMultiplicities(inlineRef.relation); - refs.push({ - name: null, - schemaName: null, - token: inlineRef.token, - endpoints: [ - { - schemaName: inlineRef.schemaName, - tableName: inlineRef.tableName, - fieldNames: inlineRef.fieldNames, - token: inlineRef.token, - relation: multiplicities[1], - }, - { - schemaName: table.schemaName, - tableName: table.name, - fieldNames: [field.name], - token: field.token, - relation: multiplicities[0], - }, - ], - }); - } - } - } - - return refs; -} diff --git a/packages/dbml-parse/src/core/option.ts b/packages/dbml-parse/src/core/option.ts deleted file mode 100644 index eb9242112..000000000 --- a/packages/dbml-parse/src/core/option.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Similar to Rust Option: https://doc.rust-lang.org/std/option/enum.Option.html - -export type Option = Some | None; - -export class Some { - value: T; - - constructor (value: T) { - this.value = value; - } - - unwrap (): T { - return this.value; - } - - unwrap_or(orValue: S): S | T { - return this.value; - } - - and_then(callback: (_: T) => Option): Option { - return callback(this.value); - } - - map(callback: (_: T) => S): Option { - return new Some(callback(this.value)); - } - - isOk (): boolean { - return true; - } -} - -export class None { - // add `value` for direct access (same api with `Some`) - value = undefined; - - unwrap (): T { - throw new Error('Trying to unwrap a None value'); - } - - unwrap_or(orValue: S): S | T { - return orValue; - } - - and_then(callback: (_: T) => Option): Option { - return new None(); - } - - map(callback: (_: T) => S): Option { - return new None(); - } - - isOk (): boolean { - return false; - } -} diff --git a/packages/dbml-parse/src/core/parser/factory.ts b/packages/dbml-parse/src/core/parser/factory.ts deleted file mode 100644 index ca47a8a9c..000000000 --- a/packages/dbml-parse/src/core/parser/factory.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SyntaxNode, SyntaxNodeId, SyntaxNodeIdGenerator } from '@/core/parser/nodes'; - -export default class NodeFactory { - private generator: SyntaxNodeIdGenerator; - - constructor (generator: SyntaxNodeIdGenerator) { - this.generator = generator; - } - - create(Type: { new (args: A, id: SyntaxNodeId): T }, args: A): T { - return new Type(args, this.generator.nextId()); - } -} diff --git a/packages/dbml-parse/src/core/lexer/lexer.ts b/packages/dbml-parse/src/core/syntax/lexer/lexer.ts similarity index 97% rename from packages/dbml-parse/src/core/lexer/lexer.ts rename to packages/dbml-parse/src/core/syntax/lexer/lexer.ts index 71827d5c1..939ff2ebb 100644 --- a/packages/dbml-parse/src/core/lexer/lexer.ts +++ b/packages/dbml-parse/src/core/syntax/lexer/lexer.ts @@ -1,11 +1,9 @@ -import { CompileError, CompileErrorCode } from '@/core/errors'; -import Report from '@/core/report'; -import { isAlphaOrUnderscore, isAlphaNumeric, isDigit } from '@/core/utils'; -import { - SyntaxToken, SyntaxTokenKind, isOp, isTriviaToken, -} from '@/core/lexer/tokens'; -import { Position } from '@/core/types'; -import { isInvalidToken } from '@/core/parser/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import Report from '@/core/types/report'; +import { Position } from '@/core/types/position'; +import { isInvalidToken } from '@/core/syntax/parser/utils'; +import { isOp, isTriviaToken, SyntaxToken, SyntaxTokenKind } from '@/core/types/tokens'; +import { isAlphaNumeric, isAlphaOrUnderscore, isDigit } from '@/core/utils/chars'; export default class Lexer { private start: Position = { @@ -233,7 +231,7 @@ export default class Lexer { } gatherInvalid () { - let i; + let i: number; const newTokenList: SyntaxToken[] = []; const leadingInvalidList: SyntaxToken[] = []; diff --git a/packages/dbml-parse/src/core/lexer/utils.ts b/packages/dbml-parse/src/core/syntax/lexer/utils.ts similarity index 92% rename from packages/dbml-parse/src/core/lexer/utils.ts rename to packages/dbml-parse/src/core/syntax/lexer/utils.ts index 58e5f8c11..4ce1c31b9 100644 --- a/packages/dbml-parse/src/core/lexer/utils.ts +++ b/packages/dbml-parse/src/core/syntax/lexer/utils.ts @@ -1,5 +1,6 @@ +import { SyntaxToken } from '@/core/types/tokens'; +import { SyntaxTokenKind } from '@/index'; import { last } from 'lodash-es'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; export function hasTrailingNewLines (token: SyntaxToken): boolean { return token.trailingTrivia.find(({ kind }) => kind === SyntaxTokenKind.NEWLINE) !== undefined; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/checks/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/checks/index.ts new file mode 100644 index 000000000..f26340871 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/checks/index.ts @@ -0,0 +1,39 @@ +import { isElementNode } from '@/core/utils/expression'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import type { + SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import type { LocalModule, Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import { validateChecks } from './validate'; +import type Compiler from '@/compiler'; + +export const checksModule: LocalModule = { + validate: validateChecks, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Checks)) return Report.create(PASS_THROUGH); + if (node.name) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'A Checks shouldn\'t have a name', node.name)]); + } + return new Report(undefined); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Checks)) return Report.create(PASS_THROUGH); + if (node.alias) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'A Checks shouldn\'t have an alias', node.alias)]); + } + return new Report(undefined); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Checks)) return Report.create(PASS_THROUGH); + if (node.attributeList) { + return new Report(new Map(), [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Checks shouldn\'t have a setting list', node.attributeList)]); + } + return new Report(new Map()); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/checks/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/checks/validate.ts new file mode 100644 index 000000000..08fc96b5c --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/checks/validate.ts @@ -0,0 +1,73 @@ +import { isElementNode } from '@/core/utils/expression'; +import { last, partition } from 'lodash-es'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, + ElementDeclarationNode, + FunctionApplicationNode, + FunctionExpressionNode, + ListExpressionNode, + ProgramNode, + AttributeNode, + SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { isExpressionAQuotedString } from '@/core/utils/expression'; +import { collectSettings } from '@/core/utils/validate'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler'; + +export function validateChecks (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Checks)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: must be inside a Table or TablePartial + const parent = node.parentElement; + if (parent instanceof ProgramNode || !(parent instanceof ElementDeclarationNode && parent.isKind(ElementKind.Table, ElementKind.TablePartial))) { + errors.push(new CompileError(CompileErrorCode.INVALID_CHECKS_CONTEXT, 'A Checks can only appear inside a Table or a TablePartial', node)); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body: must be a block + const body = node.body; + if (body instanceof FunctionApplicationNode) { + errors.push(new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A Checks must have a complex body', body)); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + + // Validate each check field + for (const field of fields as FunctionApplicationNode[]) { + if (!field.callee) continue; + const args = [field.callee, ...field.args]; + if (last(args) instanceof ListExpressionNode) { + const settingReport = collectSettings(args.pop() as ListExpressionNode); + errors.push(...settingReport.getErrors()); + const settingMap = settingReport.getValue(); + for (const [name, attrs] of settingMap) { + if (name === SettingName.Name) { + if (attrs.length > 1) attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_CHECK_SETTING, `'${name}' can only appear once`, attr))); + attrs.forEach((attr: AttributeNode) => { if (!isExpressionAQuotedString(attr.value)) errors.push(new CompileError(CompileErrorCode.INVALID_CHECK_SETTING_VALUE, `'${name}' must be a string`, attr)); }); + } else { + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_CHECK_SETTING, `Unknown check setting '${name}'`, attr))); + } + } + } + if (args.length > 1 || !(args[0] instanceof FunctionExpressionNode)) { + errors.push(new CompileError(CompileErrorCode.INVALID_CHECKS_FIELD, 'A check field must be a function expression', field)); + } + } + + // Validate sub-elements + for (const sub of subs as ElementDeclarationNode[]) { + if (sub.type) errors.push(...compiler.validate(sub).getErrors()); + } + } + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/custom/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/custom/index.ts new file mode 100644 index 000000000..72e1c374c --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/custom/index.ts @@ -0,0 +1,38 @@ +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import type { + SyntaxNode, +} from '@/core/types/nodes'; +import type { LocalModule, Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import { validateCustom } from './validate'; +import type Compiler from '@/compiler'; +import { isCustomElement } from './utils'; + +export const customModule: LocalModule = { + validate: validateCustom, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isCustomElement(node)) return Report.create(PASS_THROUGH); + if (node.name) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'A custom field shouldn\'t have a name', node.name)]); + } + return new Report(undefined); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isCustomElement(node)) return Report.create(PASS_THROUGH); + if (node.alias) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'A custom field shouldn\'t have an alias', node.alias)]); + } + return new Report(undefined); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isCustomElement(node)) return Report.create(PASS_THROUGH); + if (node.attributeList) { + return new Report(new Map(), [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A custom field shouldn\'t have a setting list', node.attributeList)]); + } + return new Report(new Map()); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/custom/utils.ts b/packages/dbml-parse/src/core/syntax/local_modules/custom/utils.ts new file mode 100644 index 000000000..8970f7581 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/custom/utils.ts @@ -0,0 +1,5 @@ +import { ElementDeclarationNode, type SyntaxNode } from '@/core/types/nodes'; + +export function isCustomElement (node: SyntaxNode): node is ElementDeclarationNode { + return node instanceof ElementDeclarationNode && !!node.type?.value; +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/custom/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/custom/validate.ts new file mode 100644 index 000000000..c953a4c3b --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/custom/validate.ts @@ -0,0 +1,44 @@ +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ProgramNode, SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { isExpressionAQuotedString } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler'; +import { isCustomElement } from './utils'; + +export function validateCustom (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isCustomElement(node)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: must be inside a Project + const parent = node.parentElement; + if (parent instanceof ProgramNode) { + errors.push(new CompileError(CompileErrorCode.INVALID_CUSTOM_CONTEXT, `Unknown schema element type: ${node.type?.value}`, node)); + } else if (!(parent instanceof ElementDeclarationNode && parent.isKind(ElementKind.Project))) { + errors.push(new CompileError(CompileErrorCode.INVALID_CUSTOM_CONTEXT, 'A custom field can only appear in a Project', node)); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof BlockExpressionNode) { + errors.push(new CompileError(CompileErrorCode.UNEXPECTED_COMPLEX_BODY, 'A custom field can only have an inline field', body)); + } else if (body instanceof FunctionApplicationNode) { + if (!isExpressionAQuotedString(body.callee)) { + errors.push(new CompileError(CompileErrorCode.INVALID_CUSTOM_ELEMENT_VALUE, 'A custom field value can only be a string', body)); + } + if (body.args.length > 0) { + errors.push(...body.args.map((arg) => new CompileError(CompileErrorCode.INVALID_CUSTOM_ELEMENT_VALUE, 'A custom field\'s value can only be a string', arg))); + } + } + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/enum/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/enum/index.ts new file mode 100644 index 000000000..8753393b4 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/enum/index.ts @@ -0,0 +1,104 @@ +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable } from '@/core/syntax/utils'; +import { last } from 'lodash-es'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { type LocalModule, type Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + AttributeNode, ListExpressionNode, SyntaxNode, +} from '@/core/types/nodes'; +import { collectSettings, isValidName } from '@/core/utils/validate'; +import { isExpressionAQuotedString } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import { extractVariableFromExpression } from '@/core/syntax/utils'; +import { validateEnum, validateEnumField } from './validate'; +import type Compiler from '@/compiler'; + +export const enumModule: LocalModule = { + validate (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Enum)) { + return validateEnum(compiler, node); + } + if (isElementFieldNode(node, ElementKind.Enum)) { + return validateEnumField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Enum)) { + if (!node.name) { + return new Report(undefined, [new CompileError(CompileErrorCode.NAME_NOT_FOUND, 'An Enum must have a name', node)]); + } + if (!isValidName(node.name)) { + return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_NAME, 'An Enum name must be of the form or .', node.name)]); + } + return new Report(destructureComplexVariable(node.name)); + } + if (isElementFieldNode(node, ElementKind.Enum)) { + const name = extractVariableFromExpression(node.callee); + return new Report(name ? [name] : undefined); + } + return Report.create(PASS_THROUGH); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Enum)) { + if (node.alias) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'An Enum shouldn\'t have an alias', node.alias)]); + } + return new Report(undefined); + } + if (isElementFieldNode(node, ElementKind.Enum)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Enum)) { + if (node.attributeList) { + return new Report(new Map(), [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'An Enum shouldn\'t have a setting list', node.attributeList)]); + } + return new Report(new Map()); + } + if (isElementFieldNode(node, ElementKind.Enum)) { + const args = [...node.args]; + let settingsList: ListExpressionNode | undefined; + if (last(args) instanceof ListExpressionNode) { + settingsList = last(args) as ListExpressionNode; + } else if (args[0] instanceof ListExpressionNode) { + settingsList = args[0]; + } + + if (!settingsList) return new Report(new Map()); + + const settingsReport = collectSettings(settingsList); + const errors = settingsReport.getErrors(); + const settingMap = settingsReport.getValue(); + const clean: Settings = new Map(); + + for (const [name, attrs] of settingMap) { + switch (name) { + case SettingName.Note: + if (attrs.length > 1) { + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_ENUM_ELEMENT_SETTING, '\'note\' can only appear once', attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isExpressionAQuotedString(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_ENUM_ELEMENT_SETTING, '\'note\' must be a string', attr)); + } + }); + clean.set(name, attrs); + break; + default: + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_ENUM_ELEMENT_SETTING, `Unknown enum field setting '${name}'`, attr))); + } + } + + return new Report(clean, errors); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/enum/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/enum/validate.ts new file mode 100644 index 000000000..82c16eb61 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/enum/validate.ts @@ -0,0 +1,76 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { isExpressionAVariableNode } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import { partition, last } from 'lodash-es'; +import type Compiler from '@/compiler'; + +export function validateEnum (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Enum)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: must be top-level + const parent = node.parentElement; + if (parent instanceof ElementDeclarationNode) { + errors.push(new CompileError(CompileErrorCode.INVALID_PROJECT_CONTEXT, 'An Enum can only appear top-level', node)); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + // Single field as body + const fieldErrors = compiler.validate(body).getErrors(); + errors.push(...fieldErrors); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + + if (fields.length === 0) { + errors.push(new CompileError(CompileErrorCode.EMPTY_ENUM, 'An Enum must have at least one element', node)); + } + + errors.push(...(fields as FunctionApplicationNode[]).flatMap((f) => compiler.validate(f).getErrors())); + + // Validate sub-elements + for (const sub of subs as ElementDeclarationNode[]) { + if (sub.type) errors.push(...compiler.validate(sub).getErrors()); + } + } + + return new Report(undefined, errors); +} + +export function validateEnumField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.Enum)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + if (node.callee && !isExpressionAVariableNode(node.callee)) { + errors.push(new CompileError(CompileErrorCode.INVALID_ENUM_ELEMENT_NAME, 'An enum field must be an identifier or a quoted identifier', node.callee)); + } + + errors.push(...compiler.settings(node).getErrors()); + + const args = [...node.args]; + if (last(args) instanceof ListExpressionNode) { + args.pop(); + } else if (args[0] instanceof ListExpressionNode) { + args.shift(); + } + + if (args.length > 0) { + errors.push(...args.map((arg) => new CompileError(CompileErrorCode.INVALID_ENUM_ELEMENT, 'An Enum must have only a field and optionally a setting list', arg))); + } + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/index.ts new file mode 100644 index 000000000..ea6985db3 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/index.ts @@ -0,0 +1,69 @@ +import { PASS_THROUGH, type PassThrough, type Unhandled, UNHANDLED } from '@/constants'; +import type { LocalModule, Settings } from './types'; +import { tableModule } from './table'; +import { enumModule } from './enum'; +import { recordsModule } from './records'; +import { indexesModule } from './indexes'; +import { checksModule } from './checks'; +import { refModule } from './ref'; +import { projectModule } from './project'; +import { tableGroupModule } from './tableGroup'; +import { tablePartialModule } from './tablePartial'; +import { noteModule } from './note'; +import { programModule } from './program'; +import type Compiler from '@/compiler'; +import type { SyntaxNode } from '@/core/types/nodes'; +import Report from '@/core/types/report'; + +// Each time you add a new element, register its module here. +export const modules: LocalModule[] = [ + tableModule, + enumModule, + recordsModule, + indexesModule, + checksModule, + refModule, + projectModule, + tableGroupModule, + tablePartialModule, + noteModule, + programModule, +]; + +// Chain-of-responsibility: iterate modules until one handles the node (returns non-PASS_THROUGH) +function dispatch ( + method: K, + ...args: Parameters> +): ReturnType> | Report { + for (const module of modules) { + const fn = module[method] as any; + if (fn) { + const result = fn(...args); + if (!result.hasValue(PASS_THROUGH)) { + return result; + } + } + } + + return Report.create(PASS_THROUGH); +} + +export function validate (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('validate', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} + +export function settings (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('settings', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} + +export function fullname (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('fullname', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} + +export function alias (this: Compiler, node: SyntaxNode): Report | Report { + const res = dispatch('alias', this, node); + return res.hasValue(PASS_THROUGH) ? Report.create(UNHANDLED) : res; +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/indexes/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/indexes/index.ts new file mode 100644 index 000000000..4a5b9e0ea --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/indexes/index.ts @@ -0,0 +1,122 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { last } from 'lodash-es'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { + AttributeNode, + ListExpressionNode, + type SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { type LocalModule, type Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { collectSettings, isVoid } from '@/core/utils/validate'; +import { isExpressionAQuotedString, isExpressionAVariableNode } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import { validateIndexes, validateIndexesField } from './validate'; +import type Compiler from '@/compiler'; + +export const indexesModule: LocalModule = { + validate (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Indexes)) { + return validateIndexes(compiler, node); + } + if (isElementFieldNode(node, ElementKind.Indexes)) { + return validateIndexesField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Indexes)) { + if (node.name) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'An Indexes shouldn\'t have a name', node.name)]); + } + return new Report(undefined); + } + if (isElementFieldNode(node, ElementKind.Indexes)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Indexes)) { + if (node.alias) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'An Indexes shouldn\'t have an alias', node.alias)]); + } + return new Report(undefined); + } + if (isElementFieldNode(node, ElementKind.Indexes)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Indexes)) { + if (node.attributeList) { + return new Report(new Map(), [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'An Indexes shouldn\'t have a setting list', node.attributeList)]); + } + return new Report(new Map()); + } + if (isElementFieldNode(node, ElementKind.Indexes)) { + const args = [node.callee, ...node.args]; + let settingsList: ListExpressionNode | undefined; + if (last(args) instanceof ListExpressionNode) { + settingsList = last(args) as ListExpressionNode; + } + + if (!settingsList) return new Report(new Map()); + + const settingsReport = collectSettings(settingsList); + const errors = settingsReport.getErrors(); + const settingMap = settingsReport.getValue(); + const clean: Settings = new Map(); + + for (const [name, attrs] of settingMap) { + switch (name) { + case SettingName.Note: + case SettingName.Name: + if (attrs.length > 1) { + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_INDEX_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isExpressionAQuotedString(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_INDEX_SETTING_VALUE, `'${name}' must be a string`, attr)); + } + }); + clean.set(name, attrs); + break; + case SettingName.Unique: + case SettingName.PK: + if (attrs.length > 1) { + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_INDEX_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_INDEX_SETTING_VALUE, `'${name}' must not have a value`, attr)); + } + }); + clean.set(name, attrs); + break; + case SettingName.Type: + if (attrs.length > 1) { + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_INDEX_SETTING, '\'type\' can only appear once', attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isExpressionAVariableNode(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_INDEX_SETTING_VALUE, '\'type\' must be "btree" or "hash"', attr)); + } + }); + clean.set(name, attrs); + break; + default: + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_INDEX_SETTING, `Unknown index setting '${name}'`, attr))); + } + } + + return new Report(clean, errors); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/indexes/utils.ts b/packages/dbml-parse/src/core/syntax/local_modules/indexes/utils.ts new file mode 100644 index 000000000..2c984865c --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/indexes/utils.ts @@ -0,0 +1,11 @@ +import { PrimaryExpressionNode, type SyntaxNode, VariableNode } from '@/core/types/nodes'; + +export function isValidIndexesType (value?: SyntaxNode): boolean { + if (!(value instanceof PrimaryExpressionNode) || !(value.expression instanceof VariableNode)) { + return false; + } + + const str = value.expression.variable?.value; + + return str === 'btree' || str === 'hash'; +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/indexes/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/indexes/validate.ts new file mode 100644 index 000000000..bd2eed7b0 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/indexes/validate.ts @@ -0,0 +1,85 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureIndexNode } from '@/core/syntax/utils'; +import { last, partition } from 'lodash-es'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, + CallExpressionNode, + ElementDeclarationNode, + FunctionApplicationNode, + ListExpressionNode, + ProgramNode, + SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler'; + +export function validateIndexes (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Indexes)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: must be inside a Table or TablePartial + const parent = node.parentElement; + if (parent instanceof ProgramNode || !(parent instanceof ElementDeclarationNode && parent.isKind(ElementKind.Table, ElementKind.TablePartial))) { + errors.push(new CompileError( + CompileErrorCode.INVALID_INDEXES_CONTEXT, + 'An Indexes can only appear inside a Table or a TablePartial', + node, + )); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + errors.push(new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'An Indexes must have a complex body', body)); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + + // Validate fields + errors.push(...(fields as FunctionApplicationNode[]).flatMap((f) => compiler.validate(f).getErrors())); + + // Validate sub-elements + for (const sub of subs as ElementDeclarationNode[]) { + if (sub.type) errors.push(...compiler.validate(sub).getErrors()); + } + } + + return new Report(undefined, errors); +} + +export function validateIndexesField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.Indexes)) return Report.create(PASS_THROUGH); + if (!node.callee) return new Report(undefined); + + const errors: CompileError[] = []; + + errors.push(...compiler.settings(node).getErrors()); + + const args = [node.callee, ...node.args]; + if (last(args) instanceof ListExpressionNode) { + args.pop(); + } + + args.forEach((sub) => { + while (sub instanceof CallExpressionNode) { + if (sub.argumentList && destructureIndexNode(sub.argumentList) === undefined) { + errors.push(new CompileError(CompileErrorCode.INVALID_INDEXES_FIELD, 'An index field must be an identifier, a quoted identifier, a functional expression or a tuple of such', sub.argumentList)); + } + sub = sub.callee!; + } + + if (destructureIndexNode(sub) === undefined) { + errors.push(new CompileError(CompileErrorCode.INVALID_INDEXES_FIELD, 'An index field must be an identifier, a quoted identifier, a functional expression or a tuple of such', sub)); + } + }); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/note/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/note/index.ts new file mode 100644 index 000000000..bfedaa212 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/note/index.ts @@ -0,0 +1,53 @@ +import { isElementNode } from '@/core/utils/expression'; +import { destructureComplexVariable } from '@/core/syntax/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { + ProgramNode, SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { type LocalModule, type Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import { validateNote } from './validate'; +import type Compiler from '@/compiler'; + +export const noteModule: LocalModule = { + validate: validateNote, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Note)) return Report.create(PASS_THROUGH); + + const parent = node.parentElement; + if (!(parent instanceof ProgramNode)) { + if (node.name) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_NAME, 'A Note shouldn\'t have a name', node.name)]); + } + return new Report(undefined); + } + + if (!node.name) { + return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_NAME, 'Sticky note must have a name', node)]); + } + + const nameFragments = destructureComplexVariable(node.name); + if (nameFragments === undefined) return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_NAME, 'Invalid name for sticky note ', node)]); + + return new Report(nameFragments); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Note)) return Report.create(PASS_THROUGH); + if (node.alias) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'A Ref shouldn\'t have an alias', node.alias)]); + } + return new Report(undefined); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Note)) return Report.create(PASS_THROUGH); + if (node.attributeList) { + return new Report(new Map(), [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Note shouldn\'t have a setting list', node.attributeList)]); + } + return new Report(new Map()); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/note/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/note/validate.ts new file mode 100644 index 000000000..4e3a2f86a --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/note/validate.ts @@ -0,0 +1,78 @@ +import { isElementNode } from '@/core/utils/expression'; +import { partition } from 'lodash-es'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ProgramNode, SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { isExpressionAQuotedString } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler'; + +export function validateNote (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Note)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context validation + const parent = node.parentElement; + if ( + !(parent instanceof ProgramNode) + && !(parent instanceof ElementDeclarationNode && parent.isKind( + ElementKind.Table, + ElementKind.TableGroup, + ElementKind.TablePartial, + ElementKind.Project, + )) + ) { + errors.push(new CompileError( + CompileErrorCode.INVALID_NOTE_CONTEXT, + 'A Note can only appear inside a Table, a TableGroup, a TablePartial or a Project. Sticky note can only appear at the global scope.', + node, + )); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + // Single field + const fields = [body]; + if (!isExpressionAQuotedString(fields[0].callee)) { + errors.push(new CompileError(CompileErrorCode.INVALID_NOTE, 'A Note content must be a quoted string', fields[0])); + } + if (fields[0].args.length > 0) { + errors.push(...fields[0].args.map((arg) => new CompileError(CompileErrorCode.INVALID_NOTE, 'A Note can only contain one quoted string', arg))); + } + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + + // Validate fields + const fieldList = fields as FunctionApplicationNode[]; + if (fieldList.length === 0) { + errors.push(new CompileError(CompileErrorCode.EMPTY_NOTE, 'A Note must have a content', node)); + } else { + if (fieldList.length > 1) { + fieldList.slice(1).forEach((field) => errors.push(new CompileError(CompileErrorCode.NOTE_CONTENT_REDEFINED, 'A Note can only contain one string', field))); + } + if (!isExpressionAQuotedString(fieldList[0].callee)) { + errors.push(new CompileError(CompileErrorCode.INVALID_NOTE, 'A Note content must be a quoted string', fieldList[0])); + } + if (fieldList[0].args.length > 0) { + errors.push(...fieldList[0].args.map((arg) => new CompileError(CompileErrorCode.INVALID_NOTE, 'A Note can only contain one quoted string', arg))); + } + } + + // Validate sub-elements + for (const sub of subs as ElementDeclarationNode[]) { + if (sub.type) errors.push(...compiler.validate(sub).getErrors()); + } + } + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/program/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/program/index.ts new file mode 100644 index 000000000..ce41e2a09 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/program/index.ts @@ -0,0 +1,26 @@ +import { isProgramNode } from '@/core/utils/expression'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { type LocalModule, type Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import { validateProgram } from './validate'; +import type Compiler from '@/compiler'; + +export const programModule: LocalModule = { + validate: validateProgram, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isProgramNode(node)) return Report.create(PASS_THROUGH); + return new Report(undefined); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isProgramNode(node)) return Report.create(PASS_THROUGH); + return new Report(undefined); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isProgramNode(node)) return Report.create(PASS_THROUGH); + return new Report(new Map()); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/program/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/program/validate.ts new file mode 100644 index 000000000..faea39fe1 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/program/validate.ts @@ -0,0 +1,31 @@ +import { isProgramNode } from '@/core/utils/expression'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import Report from '@/core/types/report'; +import type Compiler from '@/compiler'; + +export function validateProgram (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isProgramNode(node)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Validate each top-level element + node.body.forEach((element) => { + if (!element.type) return; + errors.push(...compiler.validate(element).getErrors()); + }); + + // Only one project allowed + const projects = node.body.filter( + (e) => e.type?.value.toLowerCase() === ElementKind.Project, + ); + if (projects.length > 1) { + projects.forEach((project) => errors.push( + new CompileError(CompileErrorCode.PROJECT_REDEFINED, 'Only one project can exist', project), + )); + } + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/project/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/project/index.ts new file mode 100644 index 000000000..6f6ad92d8 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/project/index.ts @@ -0,0 +1,65 @@ +import { ElementKind } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable } from '@/core/syntax/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { type LocalModule, type Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + SyntaxNode, +} from '@/core/types/nodes'; +import { isSimpleName } from '@/core/utils/validate'; +import Report from '@/core/types/report'; +import { validateProject, validateProjectField } from './validate'; +import type Compiler from '@/compiler'; + +export const projectModule: LocalModule = { + validate (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Project)) { + return validateProject(compiler, node); + } + if (isElementFieldNode(node, ElementKind.Project)) { + return validateProjectField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Project)) { + if (!node.name) return new Report(undefined); + if (!isSimpleName(node.name)) { + return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_NAME, 'A Project\'s name is optional or must be an identifier or a quoted identifer', node.name)]); + } + return new Report(destructureComplexVariable(node.name)); + } + if (isElementFieldNode(node, ElementKind.Project)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Project)) { + if (node.alias) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'A Project shouldn\'t have an alias', node.alias)]); + } + return new Report(undefined); + } + if (isElementFieldNode(node, ElementKind.Project)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Project)) { + if (node.attributeList) { + return new Report(new Map(), [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Project shouldn\'t have a setting list', node.attributeList)]); + } + return new Report(new Map()); + } + if (isElementFieldNode(node, ElementKind.Project)) { + return new Report(new Map()); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/project/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/project/validate.ts new file mode 100644 index 000000000..3e2030ac5 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/project/validate.ts @@ -0,0 +1,50 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import Report from '@/core/types/report'; +import { partition } from 'lodash-es'; +import type Compiler from '@/compiler'; + +export function validateProject (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Project)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: must be top-level + const parent = node.parentElement; + if (parent instanceof ElementDeclarationNode) { + errors.push(new CompileError(CompileErrorCode.INVALID_PROJECT_CONTEXT, 'A Project can only appear top-level', node)); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + errors.push(new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A Project\'s body must be a block', body)); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + + // Project can't have inline fields + errors.push(...fields.map((field) => new CompileError(CompileErrorCode.INVALID_PROJECT_FIELD, 'A Project can not have inline fields', field))); + + // Validate sub-elements + for (const sub of subs as ElementDeclarationNode[]) { + if (sub.type) errors.push(...compiler.validate(sub).getErrors()); + } + } + + return new Report(undefined, errors); +} + +export function validateProjectField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.Project)) return Report.create(PASS_THROUGH); + return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_PROJECT_FIELD, 'A Project can not have inline fields', node)]); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/records/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/records/index.ts new file mode 100644 index 000000000..9493e34bd --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/records/index.ts @@ -0,0 +1,102 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable } from '@/core/syntax/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { + CallExpressionNode, ProgramNode, SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { type LocalModule, type Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { isValidName } from '@/core/utils/validate'; +import { isTupleOfVariables } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import { validateRecords, validateRecordsField } from './validate'; +import type Compiler from '@/compiler'; + +export const recordsModule: LocalModule = { + validate (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Records)) { + return validateRecords(compiler, node); + } + if (isElementFieldNode(node, ElementKind.Records)) { + return validateRecordsField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Records)) { + const parent = node.parentElement; + const isTopLevel = parent instanceof ProgramNode; + + if (isTopLevel) { + // Top-level: must have name in form table(col1, col2, ...) + if (!(node.name instanceof CallExpressionNode)) { + return new Report(undefined, [new CompileError( + CompileErrorCode.INVALID_RECORDS_NAME, + 'Records at top-level must have a name in the form of table(col1, col2, ...) or schema.table(col1, col2, ...)', + node.name || node.type || node, + )]); + } + + const errs: CompileError[] = []; + if (!node.name.callee || !isValidName(node.name.callee)) { + errs.push(new CompileError( + CompileErrorCode.INVALID_RECORDS_NAME, + 'Records table reference must be a valid table name', + node.name.callee || node.name, + )); + } + if (!node.name.argumentList || !isTupleOfVariables(node.name.argumentList)) { + errs.push(new CompileError( + CompileErrorCode.INVALID_RECORDS_NAME, + 'Records column list must be simple column names', + node.name.argumentList || node.name, + )); + } + + return new Report(destructureComplexVariable(node.name), errs); + } else { + // Inside a table: optional column list only + if (node.name && !isTupleOfVariables(node.name)) { + return new Report(undefined, [new CompileError( + CompileErrorCode.INVALID_RECORDS_NAME, + 'Records inside a Table can only have a column list like (col1, col2, ...)', + node.name, + )]); + } + return new Report(destructureComplexVariable(node.name)); + } + } + if (isElementFieldNode(node, ElementKind.Records)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Records)) { + if (node.alias) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'Records cannot have an alias', node.alias)]); + } + return new Report(undefined); + } + if (isElementFieldNode(node, ElementKind.Records)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Records)) { + if (node.attributeList) { + return new Report(new Map(), [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'Records cannot have a setting list', node.attributeList)]); + } + return new Report(new Map()); + } + if (isElementFieldNode(node, ElementKind.Records)) { + return new Report(new Map()); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/records/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/records/validate.ts new file mode 100644 index 000000000..34143d14c --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/records/validate.ts @@ -0,0 +1,113 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable } from '@/core/syntax/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, CommaExpressionNode, ElementDeclarationNode, EmptyNode, FunctionApplicationNode, FunctionExpressionNode, ProgramNode, SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { KEYWORDS_OF_DEFAULT_SETTING } from '@/constants'; +import { isAccessExpression, isExpressionAQuotedString, isExpressionASignedNumberExpression, isExpressionAVariableNode } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import { partition } from 'lodash-es'; +import type Compiler from '@/compiler'; + +function isValidRecordValue (value: SyntaxNode): boolean { + if (value instanceof EmptyNode) return true; + if (isExpressionASignedNumberExpression(value)) return true; + if (isExpressionAQuotedString(value)) return true; + if (value instanceof FunctionExpressionNode) return true; + if (isExpressionAVariableNode(value)) { + const identifierValue = value.expression.variable.value.toLowerCase(); + return KEYWORDS_OF_DEFAULT_SETTING.includes(identifierValue); + } + if (isAccessExpression(value)) { + const fragments = destructureComplexVariable(value); + return fragments !== undefined && fragments.length > 0; + } + return false; +} + +function validateDataRow (compiler: Compiler, node: SyntaxNode, row: FunctionApplicationNode): CompileError[] { + const errors: CompileError[] = []; + + if (!row.callee || row.args.length > 0) { + errors.push(new CompileError( + CompileErrorCode.INVALID_RECORDS_FIELD, + 'Invalid record row structure', + row, + )); + return errors; + } + + if (row.callee instanceof CommaExpressionNode) { + for (const value of row.callee.elementList) { + if (!isValidRecordValue(value)) { + errors.push(new CompileError( + CompileErrorCode.INVALID_RECORDS_FIELD, + 'Records can only contain simple values (literals, null, true, false, or enum references). Complex expressions are not allowed.', + value, + )); + } + } + } else { + if (!isValidRecordValue(row.callee)) { + errors.push(new CompileError( + CompileErrorCode.INVALID_RECORDS_FIELD, + 'Records can only contain simple values (literals, null, true, false, or enum references). Complex expressions are not allowed.', + row.callee, + )); + } + } + + return errors; +} + +export function validateRecords (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Records)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: top-level or inside a Table + const parent = node.parentElement; + const isTopLevel = parent instanceof ProgramNode; + if (!isTopLevel) { + if (!(parent instanceof ElementDeclarationNode && parent.isKind(ElementKind.Table))) { + errors.push(new CompileError( + CompileErrorCode.INVALID_RECORDS_CONTEXT, + 'Records can only appear at top-level or inside a Table', + node, + )); + } + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + errors.push(...validateDataRow(compiler, node, body)); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + + // Validate data rows + for (const row of fields as FunctionApplicationNode[]) { + errors.push(...validateDataRow(compiler, node, row)); + } + + // Validate sub-elements + for (const sub of subs as ElementDeclarationNode[]) { + if (sub.type) errors.push(...compiler.validate(sub).getErrors()); + } + } + + return new Report(undefined, errors); +} + +export function validateRecordsField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.Records)) return Report.create(PASS_THROUGH); + return new Report(undefined, validateDataRow(compiler, node, node)); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/ref/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/ref/index.ts new file mode 100644 index 000000000..d9ffef982 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/ref/index.ts @@ -0,0 +1,147 @@ +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable } from '@/core/syntax/utils'; +import { last } from 'lodash-es'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { type LocalModule, type Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + AttributeNode, IdentiferStreamNode, ListExpressionNode, SyntaxNode, +} from '@/core/types/nodes'; +import { extractStringFromIdentifierStream, isExpressionAVariableNode } from '@/core/utils/expression'; +import { collectSettings, isSimpleName, isValidColor } from '@/core/utils/validate'; +import { SyntaxTokenKind } from '@/index'; +import Report from '@/core/types/report'; +import { validateRef, validateRefField } from './validate'; +import type Compiler from '@/compiler'; + +function isValidPolicy (value?: SyntaxNode): boolean { + if ( + !( + isExpressionAVariableNode(value) + && value.expression.variable.kind !== SyntaxTokenKind.QUOTED_STRING + ) + && !(value instanceof IdentiferStreamNode) + ) { + return false; + } + + let extractedString: string | undefined; + if (value instanceof IdentiferStreamNode) { + extractedString = extractStringFromIdentifierStream(value) ?? ''; + } else { + extractedString = value.expression.variable.value; + } + + if (extractedString) { + switch (extractedString.toLowerCase()) { + case 'cascade': + case 'no action': + case 'set null': + case 'set default': + case 'restrict': + return true; + default: + return false; + } + } + + return false; +} + +export const refModule: LocalModule = { + validate (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Ref)) { + return validateRef(compiler, node); + } + if (isElementFieldNode(node, ElementKind.Ref)) { + return validateRefField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Ref)) { + if (!node.name) return new Report(undefined); + if (!isSimpleName(node.name)) { + return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_NAME, 'A Ref\'s name is optional or must be an identifier or a quoted identifer', node.name)]); + } + return new Report(destructureComplexVariable(node.name)); + } + if (isElementFieldNode(node, ElementKind.Ref)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Ref)) { + if (node.alias) { + return new Report(undefined, [new CompileError(CompileErrorCode.UNEXPECTED_ALIAS, 'A Ref shouldn\'t have an alias', node.alias)]); + } + return new Report(undefined); + } + if (isElementFieldNode(node, ElementKind.Ref)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Ref)) { + if (node.attributeList) { + return new Report(new Map(), [new CompileError(CompileErrorCode.UNEXPECTED_SETTINGS, 'A Ref shouldn\'t have a setting list', node.attributeList)]); + } + return new Report(new Map()); + } + if (isElementFieldNode(node, ElementKind.Ref)) { + const args = [...node.args]; + let settingsList: ListExpressionNode | undefined; + if (last(args) instanceof ListExpressionNode) { + settingsList = last(args) as ListExpressionNode; + } else if (args[0] instanceof ListExpressionNode) { + settingsList = args[0]; + } + + if (!settingsList) return new Report(new Map()); + + const settingsReport = collectSettings(settingsList); + const errors = settingsReport.getErrors(); + const settingMap = settingsReport.getValue(); + const clean: Settings = new Map(); + + for (const [name, attrs] of settingMap) { + switch (name) { + case SettingName.Delete: + case SettingName.Update: + if (attrs.length > 1) { + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.DUPLICATE_REF_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isValidPolicy(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_REF_SETTING_VALUE, `'${name}' can only have values "cascade", "no action", "set null", "set default" or "restrict"`, attr)); + } + }); + clean.set(name, attrs); + break; + case SettingName.Color: + if (attrs.length > 1) { + errors.push(...attrs.map((attr: AttributeNode) => new CompileError(CompileErrorCode.DUPLICATE_REF_SETTING, '\'color\' can only appear once', attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isValidColor(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_REF_SETTING_VALUE, '\'color\' must be a color literal', attr!)); + } + }); + clean.set(name, attrs); + break; + default: + attrs.forEach((attr: AttributeNode) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_REF_SETTING, `Unknown ref setting '${name}'`, attr))); + } + } + + return new Report(clean, errors); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/ref/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/ref/validate.ts new file mode 100644 index 000000000..44d3c3ad0 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/ref/validate.ts @@ -0,0 +1,111 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable, destructureComplexVariableTuple, isBinaryRelationship, isEqualTupleOperands } from '@/core/syntax/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, ListExpressionNode, ProgramNode, SyntaxNode, TupleExpressionNode, +} from '@/core/types/nodes'; + +// Check if a ref endpoint is a valid column reference: +// dotted chain like a.b.c, or standalone tuple of dotted chains like (a.b, c.d) +function isValidRefColumnReference (node?: SyntaxNode): boolean { + if (!node) return false; + const fragment = destructureComplexVariableTuple(node); + if (fragment) { + const count = fragment.variables.length + Math.min(fragment.tupleElements.length, 1); + return count >= 2; + } + // Standalone tuple of dotted chains + if (node instanceof TupleExpressionNode) { + return node.elementList.length > 0 && node.elementList.every((e) => { + const v = destructureComplexVariable(e); + return v !== undefined && v.length >= 2; + }); + } + return false; +} +import { ElementKind } from '@/core/types/keywords'; +import Report from '@/core/types/report'; +import { partition, last } from 'lodash-es'; +import type Compiler from '@/compiler'; + +export function validateRef (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Ref)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: must be top-level + const parent = node.parentElement; + if (!(parent instanceof ProgramNode)) { + errors.push(new CompileError(CompileErrorCode.INVALID_REF_CONTEXT, 'A Ref must appear top-level', node)); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + errors.push(...compiler.validate(body).getErrors()); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + + if (fields.length === 0) { + errors.push(new CompileError(CompileErrorCode.EMPTY_REF, 'A Ref must have at least one field', node)); + } + if (fields.length > 1) { + errors.push(...fields.slice(1).map((field) => new CompileError(CompileErrorCode.REF_REDEFINED, 'A Ref can only contain one binary relationship', field))); + } + + errors.push(...(fields as FunctionApplicationNode[]).flatMap((f) => compiler.validate(f).getErrors())); + + // Validate sub-elements + for (const sub of subs as ElementDeclarationNode[]) { + if (sub.type) errors.push(...compiler.validate(sub).getErrors()); + } + } + + return new Report(undefined, errors); +} + +export function validateRefField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.Ref)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + if (node.callee && !isBinaryRelationship(node.callee)) { + errors.push(new CompileError(CompileErrorCode.INVALID_REF_FIELD, 'A Ref field must be a binary relationship', node.callee)); + } + + if (node.callee && isBinaryRelationship(node.callee)) { + const leftOk = isValidRefColumnReference(node.callee.leftExpression); + const rightOk = isValidRefColumnReference(node.callee.rightExpression); + if (!leftOk) { + errors.push(new CompileError(CompileErrorCode.INVALID_REF_FIELD, 'Invalid column reference', node.callee.leftExpression || node.callee)); + } + if (!rightOk) { + errors.push(new CompileError(CompileErrorCode.INVALID_REF_FIELD, 'Invalid column reference', node.callee.rightExpression || node.callee)); + } + } + + if (node.callee && !isEqualTupleOperands(node.callee)) { + errors.push(new CompileError(CompileErrorCode.UNEQUAL_FIELDS_BINARY_REF, 'Unequal fields in ref endpoints', node.callee)); + } + + errors.push(...compiler.settings(node).getErrors()); + + const args = [...node.args]; + if (last(args) instanceof ListExpressionNode) { + args.pop(); + } else if (args[0] instanceof ListExpressionNode) { + args.shift(); + } + + if (args.length > 0) { + errors.push(...args.map((arg) => new CompileError(CompileErrorCode.INVALID_REF_FIELD, 'A Ref field should only have a single binary relationship', arg))); + } + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/table/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/table/index.ts new file mode 100644 index 000000000..4cf54709d --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/table/index.ts @@ -0,0 +1,275 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable, extractVariableFromExpression, extractVarNameFromPrimaryVariable } from '@/core/syntax/utils'; +import { last } from 'lodash-es'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import type { LocalModule, Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + ArrayNode, + AttributeNode, + FunctionExpressionNode, + ListExpressionNode, + PrimaryExpressionNode, + SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { collectSettings, isUnaryRelationship, isValidAlias, isValidColor, isValidColumnType, isValidDefaultValue, isValidName, isValidPartialInjection, isVoid } from '@/core/utils/validate'; +import { isExpressionAnIdentifierNode, isExpressionAQuotedString, isExpressionAVariableNode } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import { validateTable, validateTableField } from './validate'; +import type Compiler from '@/compiler'; + +export const tableModule: LocalModule = { + validate (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Table)) { + return validateTable(compiler, node); + } + if (isElementFieldNode(node, ElementKind.Table)) { + return validateTableField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Table)) { + if (!node.name) { + return new Report(undefined, [new CompileError(CompileErrorCode.NAME_NOT_FOUND, 'A Table must have a name', node)]); + } + if (node.name instanceof ArrayNode) { + return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_NAME, 'Invalid array as Table name, maybe you forget to add a space between the name and the setting list?', node.name)]); + } + if (!isValidName(node.name)) { + return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_NAME, 'A Table name must be of the form
or .
', node.name)]); + } + return new Report(destructureComplexVariable(node.name)); + } + if (isElementFieldNode(node, ElementKind.Table)) { + const name = extractVariableFromExpression(node.callee); + return new Report(name ? [name] : undefined); + } + return Report.create(PASS_THROUGH); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Table)) { + if (!node.alias) return new Report(undefined); + if (!isValidAlias(node.alias)) { + return new Report(undefined, [new CompileError(CompileErrorCode.INVALID_ALIAS, 'Table aliases can only contains alphanumeric and underscore unless surrounded by double quotes', node.alias)]); + } + return new Report(extractVariableFromExpression(node.alias)); + } + if (isElementFieldNode(node, ElementKind.Table)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.Table)) { + if (!node.attributeList) return new Report(new Map()); + + const settingsReport = collectSettings(node.attributeList); + const errors = settingsReport.getErrors(); + const settingMap = settingsReport.getValue(); + const clean: Settings = new Map(); + + for (const [name, attrs] of settingMap) { + switch (name) { + case SettingName.HeaderColor: + if (attrs.length > 1) { + errors.push(...attrs.map((attr: AttributeNode) => new CompileError(CompileErrorCode.DUPLICATE_TABLE_SETTING, '\'headercolor\' can only appear once', attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isValidColor(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_SETTING_VALUE, '\'headercolor\' must be a color literal', attr.value || attr.name!)); + } + }); + clean.set(name, attrs); + break; + case SettingName.Note: + if (attrs.length > 1) { + errors.push(...attrs.map((attr: AttributeNode) => new CompileError(CompileErrorCode.DUPLICATE_TABLE_SETTING, '\'note\' can only appear once', attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isExpressionAQuotedString(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_SETTING_VALUE, '\'note\' must be a string literal', attr.value || attr.name!)); + } + }); + clean.set(name, attrs); + break; + default: + errors.push(...attrs.map((attr: AttributeNode) => new CompileError(CompileErrorCode.UNKNOWN_TABLE_SETTING, `Unknown '${name}' setting`, attr))); + } + } + + return new Report(clean, errors); + } + if (isElementFieldNode(node, ElementKind.Table)) { + const errors: CompileError[] = []; + const map = new Map(); + + const remains = node.args.slice(1); + + if (!remains.slice(0, -1).every(isExpressionAnIdentifierNode) || !remains.slice(-1).every((p) => isExpressionAnIdentifierNode(p) || p instanceof ListExpressionNode)) { + return new Report(new Map(), remains.map((part) => new CompileError(CompileErrorCode.INVALID_COLUMN, 'These fields must be some inline settings optionally ended with a setting list', part))); + } + + if (remains.length === 0) { + return new Report(map as Settings); + } + + let settingList: ListExpressionNode | undefined; + if (last(remains) instanceof ListExpressionNode) { + settingList = remains.pop() as ListExpressionNode; + } + + const settingsReport = collectSettings(settingList); + errors.push(...settingsReport.getErrors()); + const settingMap = settingsReport.getValue() as Map; + + remains.forEach((part) => { + const name = (extractVarNameFromPrimaryVariable(part as any) ?? '').toLowerCase(); + if (name !== 'pk' && name !== 'unique') { + errors.push(new CompileError(CompileErrorCode.INVALID_SETTINGS, 'Inline column settings can only be `pk` or `unique`', part)); + return; + } + if (!settingMap.has(name)) { + settingMap.set(name, [part as PrimaryExpressionNode]); + } else { + settingMap.get(name)!.push(part as PrimaryExpressionNode); + } + }); + + const pkAttrs = settingMap.get(SettingName.PK) || []; + const pkeyAttrs = settingMap.get(SettingName.PrimaryKey) || []; + if (pkAttrs.length >= 1 && pkeyAttrs.length >= 1) { + errors.push( + ...[...pkAttrs, ...pkeyAttrs] + .map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, 'Either one of \'primary key\' and \'pk\' can appear', attr)), + ); + } + + for (const [name, _attrs] of settingMap) { + const attrs = _attrs as AttributeNode[]; + switch (name) { + case SettingName.Note: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, 'note can only appear once', attr))); + } + attrs.forEach((attr) => { + if (!isExpressionAQuotedString(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'note\' must be a quoted string', attr.value || attr.name!)); + } + }); + break; + case SettingName.Ref: + attrs.forEach((attr) => { + if (!isUnaryRelationship(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'ref\' must be a valid unary relationship', attr.value || attr.name!)); + } + }); + break; + case SettingName.PrimaryKey: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, 'primary key can only appear once', attr))); + } + attrs.forEach((attr) => { + if (!isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'primary key\' must not have a value', attr.value || attr.name!)); + } + }); + break; + case SettingName.PK: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'pk\' can only appear once', attr))); + } + attrs.forEach((attr) => { + if (attr instanceof AttributeNode && !isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'pk\' must not have a value', attr.value || attr.name!)); + } + }); + break; + case SettingName.NotNull: { + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'not null\' can only appear once', attr))); + } + const nullAttrs = settingMap.get(SettingName.Null) || []; + if (attrs.length >= 1 && nullAttrs.length >= 1) { + errors.push( + ...[...attrs, ...nullAttrs] + .map((attr) => new CompileError(CompileErrorCode.CONFLICTING_SETTING, '\'not null\' and \'null\' can not be set at the same time', attr)), + ); + } + attrs.forEach((attr) => { + if (!isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'not null\' must not have a value', attr.value || attr.name!)); + } + }); + break; + } + case SettingName.Null: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'null\' can only appear once', attr))); + } + attrs.forEach((attr) => { + if (!isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'null\' must not have a value', attr.value || attr.name!)); + } + }); + break; + case SettingName.Unique: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'unique\' can only appear once', attr))); + } + attrs.forEach((attr) => { + if (attr instanceof AttributeNode && !isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'unique\' must not have a value', attr.value || attr.name!)); + } + }); + break; + case SettingName.Increment: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'increment\' can only appear once', attr))); + } + attrs.forEach((attr) => { + if (attr instanceof AttributeNode && !isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'increment\' must not have a value', attr.value || attr.name!)); + } + }); + break; + case SettingName.Default: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, '\'default\' can only appear once', attr))); + } + attrs.forEach((attr) => { + if (!isValidDefaultValue(attr.value)) { + errors.push(new CompileError( + CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, + '\'default\' must be an enum value, a string literal, number literal, function expression, true, false or null', + attr.value || attr.name!, + )); + } + }); + break; + case SettingName.Check: + attrs.forEach((attr) => { + if (!(attr.value instanceof FunctionExpressionNode)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'check\' must be a function expression', attr.value || attr.name!)); + } + }); + break; + + default: + attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_COLUMN_SETTING, `Unknown column setting '${name}'`, attr))); + } + } + + for (const [name, attrs] of settingMap) { + map.set(name, attrs as AttributeNode[]); + } + + return new Report(map as Settings, errors); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/table/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/table/validate.ts new file mode 100644 index 000000000..2eb78496f --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/table/validate.ts @@ -0,0 +1,115 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, + ElementDeclarationNode, + FunctionApplicationNode, + PrefixExpressionNode, + SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { isValidPartialInjection, isValidColumnType } from '@/core/utils/validate'; +import { isExpressionAVariableNode } from '@/core/utils/expression'; +import { extractVariableFromExpression } from '@/core/syntax/utils'; +import Report from '@/core/types/report'; +import { partition } from 'lodash-es'; +import type Compiler from '@/compiler'; + +function validateTableFieldPartialInjection (field: FunctionApplicationNode): CompileError[] { + const errors: CompileError[] = []; + if (!field.callee) { + return []; + } + if (!isValidPartialInjection(field.callee)) { + errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_PARTIAL_INJECTION, 'A partial injection should be of the form ~', field.callee)); + } + if (field.args.length) { + errors.push( + ...field.args.map((arg) => new CompileError(CompileErrorCode.INVALID_TABLE_PARTIAL_INJECTION, 'A partial injection does not have any trailing attributes', arg)), + ); + } + return errors; +} + +export function validateTable (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.Table)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: must be top-level + const parent = node.parentElement; + if (parent instanceof ElementDeclarationNode) { + errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_CONTEXT, 'Table must appear top-level', node)); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + errors.push(new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A Table\'s body must be a block', body)); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + errors.push(...(fields as FunctionApplicationNode[]).flatMap((f) => compiler.validate(f).getErrors())); + + // Validate sub-elements + const subErrors = (subs as ElementDeclarationNode[]).flatMap((sub) => { + if (!sub.type) return []; + return compiler.validate(sub).getErrors(); + }); + errors.push(...subErrors); + + const notes = (subs as ElementDeclarationNode[]).filter((sub) => sub.type?.value.toLowerCase() === 'note'); + if (notes.length > 1) { + errors.push(...notes.map((note) => new CompileError(CompileErrorCode.NOTE_REDEFINED, 'Duplicate notes are defined', note))); + } + + // Check for duplicate partial injections + const injections = (fields as FunctionApplicationNode[]).filter((f) => f.callee instanceof PrefixExpressionNode && f.callee.op?.value === '~'); + const seen = new Map(); + for (const inj of injections) { + const name = extractVariableFromExpression((inj.callee as PrefixExpressionNode).expression); + if (!name) continue; + if (seen.has(name)) { + errors.push(new CompileError(CompileErrorCode.DUPLICATE_TABLE_PARTIAL_INJECTION_NAME, `Duplicate table partial injection '${name}'`, inj.callee)); + const first = seen.get(name)!; + errors.push(new CompileError(CompileErrorCode.DUPLICATE_TABLE_PARTIAL_INJECTION_NAME, `Duplicate table partial injection '${name}'`, first.callee)); + } else { + seen.set(name, inj); + } + } + } + + return new Report(undefined, errors); +} + +export function validateTableField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.Table)) return Report.create(PASS_THROUGH); + if (node.callee instanceof PrefixExpressionNode && node.callee.op?.value === '~') { + return new Report(undefined, validateTableFieldPartialInjection(node)); + } + + const errors: CompileError[] = []; + if (!node.callee) { + return new Report(undefined); + } + if (node.args.length === 0) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN, 'A column must have a type', node.callee!)); + } + + if (!isExpressionAVariableNode(node.callee)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_NAME, 'A column name must be an identifier or a quoted identifier', node.callee)); + } + + if (node.args[0] && !isValidColumnType(node.args[0])) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_TYPE, 'Invalid column type', node.args[0])); + } + + errors.push(...compiler.settings(node).getErrors()); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/tableGroup/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/tableGroup/index.ts new file mode 100644 index 000000000..580529444 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/tableGroup/index.ts @@ -0,0 +1,135 @@ +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable } from '@/core/syntax/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { type LocalModule, type Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + AttributeNode, + SyntaxNode, +} from '@/core/types/nodes'; +import { isExpressionAQuotedString } from '@/core/utils/expression'; +import { collectSettings, isSimpleName, isValidColor } from '@/core/utils/validate'; +import Report from '@/core/types/report'; +import { validateTableGroup, validateTableGroupField } from './validate'; +import type Compiler from '@/compiler'; + +export const tableGroupModule: LocalModule = { + validate (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TableGroup)) { + return validateTableGroup(compiler, node); + } + if (isElementFieldNode(node, ElementKind.TableGroup)) { + return validateTableGroupField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TableGroup)) { + if (!node.name) { + return new Report(undefined, [new CompileError( + CompileErrorCode.NAME_NOT_FOUND, + 'A TableGroup must have a name', + node, + )]); + } + if (!isSimpleName(node.name)) { + return new Report(undefined, [new CompileError( + CompileErrorCode.INVALID_NAME, + 'A TableGroup name must be a single identifier', + node.name, + )]); + } + return new Report(destructureComplexVariable(node.name)); + } + if (isElementFieldNode(node, ElementKind.TableGroup)) { + return new Report(destructureComplexVariable(node.callee)); + } + return Report.create(PASS_THROUGH); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TableGroup)) { + if (node.alias) { + return new Report(undefined, [new CompileError( + CompileErrorCode.UNEXPECTED_ALIAS, + 'A TableGroup shouldn\'t have an alias', + node.alias, + )]); + } + return new Report(undefined); + } + if (isElementFieldNode(node, ElementKind.TableGroup)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TableGroup)) { + if (!node.attributeList) return new Report(new Map()); + + const settingsReport = collectSettings(node.attributeList); + const errors = settingsReport.getErrors(); + const settingMap = settingsReport.getValue(); + const clean: Settings = new Map(); + + for (const [name, attrs] of settingMap) { + switch (name) { + case SettingName.Color: + if (attrs.length > 1) { + errors.push(...attrs.map((attr: AttributeNode) => new CompileError( + CompileErrorCode.DUPLICATE_TABLE_SETTING, + '\'color\' can only appear once', + attr, + ))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isValidColor(attr.value)) { + errors.push(new CompileError( + CompileErrorCode.INVALID_TABLE_SETTING_VALUE, + '\'color\' must be a color literal', + attr.value || attr.name!, + )); + } + }); + clean.set(name, attrs); + break; + case SettingName.Note: + if (attrs.length > 1) { + errors.push(...attrs.map((attr: AttributeNode) => new CompileError( + CompileErrorCode.DUPLICATE_TABLE_SETTING, + '\'note\' can only appear once', + attr, + ))); + } + attrs + .filter((attr: AttributeNode) => !isExpressionAQuotedString(attr.value)) + .forEach((attr: AttributeNode) => { + errors.push(new CompileError( + CompileErrorCode.INVALID_TABLE_SETTING_VALUE, + '\'note\' must be a string literal', + attr.value || attr.name!, + )); + }); + clean.set(name, attrs); + break; + default: + errors.push(...attrs.map((attr: AttributeNode) => new CompileError( + CompileErrorCode.UNKNOWN_TABLE_SETTING, + `Unknown '${name}' setting`, + attr, + ))); + break; + } + } + + return new Report(clean, errors); + } + if (isElementFieldNode(node, ElementKind.TableGroup)) { + return new Report(new Map()); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/tableGroup/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/tableGroup/validate.ts new file mode 100644 index 000000000..6eb59a1af --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/tableGroup/validate.ts @@ -0,0 +1,82 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable } from '@/core/syntax/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, ElementDeclarationNode, FunctionApplicationNode, SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import Report from '@/core/types/report'; +import { partition } from 'lodash-es'; +import type Compiler from '@/compiler'; + +export function validateTableGroup (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.TableGroup)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + + // Context: must be top-level + const parent = node.parentElement; + if (parent instanceof ElementDeclarationNode) { + errors.push(new CompileError( + CompileErrorCode.INVALID_TABLEGROUP_CONTEXT, + 'TableGroup must appear top-level', + node, + )); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + errors.push(new CompileError( + CompileErrorCode.UNEXPECTED_SIMPLE_BODY, + 'A TableGroup\'s body must be a block', + body, + )); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + + // Validate fields + errors.push(...(fields as FunctionApplicationNode[]).flatMap((field) => { + const fieldErrors: CompileError[] = []; + if (field.callee && destructureComplexVariable(field.callee) === undefined) { + fieldErrors.push(new CompileError(CompileErrorCode.INVALID_TABLEGROUP_FIELD, 'A TableGroup field must be of the form
or .
', field.callee)); + } + if (field.args.length > 0) { + fieldErrors.push(...field.args.map((arg) => new CompileError(CompileErrorCode.INVALID_TABLEGROUP_FIELD, 'A TableGroup field should only have a single Table name', arg))); + } + return fieldErrors; + })); + + // Validate sub-elements + const subErrors = (subs as ElementDeclarationNode[]).flatMap((sub) => { + if (!sub.type) return []; + return compiler.validate(sub).getErrors(); + }); + errors.push(...subErrors); + + const notes = (subs as ElementDeclarationNode[]).filter((sub) => sub.type?.value.toLowerCase() === 'note'); + if (notes.length > 1) errors.push(...notes.map((note) => new CompileError(CompileErrorCode.NOTE_REDEFINED, 'Duplicate notes are defined', note))); + } + + return new Report(undefined, errors); +} + +export function validateTableGroupField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.TableGroup)) return Report.create(PASS_THROUGH); + + const errors: CompileError[] = []; + if (node.callee && destructureComplexVariable(node.callee) === undefined) { + errors.push(new CompileError(CompileErrorCode.INVALID_TABLEGROUP_FIELD, 'A TableGroup field must be of the form
or .
', node.callee)); + } + if (node.args.length > 0) { + errors.push(...node.args.map((arg) => new CompileError(CompileErrorCode.INVALID_TABLEGROUP_FIELD, 'A TableGroup field should only have a single Table name', arg))); + } + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/tablePartial/index.ts b/packages/dbml-parse/src/core/syntax/local_modules/tablePartial/index.ts new file mode 100644 index 000000000..3732e674d --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/tablePartial/index.ts @@ -0,0 +1,298 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { destructureComplexVariable, extractVariableFromExpression, extractVarNameFromPrimaryVariable } from '@/core/syntax/utils'; +import { last } from 'lodash-es'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { + AttributeNode, + FunctionExpressionNode, + ListExpressionNode, + PrimaryExpressionNode, + SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind, SettingName } from '@/core/types/keywords'; +import type { LocalModule, Settings } from '../types'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { collectSettings, isSimpleName, isUnaryRelationship, isValidColor, isValidDefaultValue, isVoid } from '@/core/utils/validate'; +import { isExpressionAnIdentifierNode, isExpressionAQuotedString } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import { validateTablePartial, validateTablePartialField } from './validate'; +import type Compiler from '@/compiler'; + +export const tablePartialModule: LocalModule = { + validate (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TablePartial)) { + return validateTablePartial(compiler, node); + } + if (isElementFieldNode(node, ElementKind.TablePartial)) { + return validateTablePartialField(compiler, node); + } + return Report.create(PASS_THROUGH); + }, + + fullname (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TablePartial)) { + if (!node.name) { + return new Report(undefined, [new CompileError( + CompileErrorCode.NAME_NOT_FOUND, + 'A TablePartial must have a name', + node, + )]); + } + if (!isSimpleName(node.name)) { + return new Report(undefined, [new CompileError( + CompileErrorCode.INVALID_NAME, + 'A TablePartial name must be an identifier or a quoted identifer', + node.name, + )]); + } + return new Report(destructureComplexVariable(node.name)); + } + if (isElementFieldNode(node, ElementKind.TablePartial)) { + const name = extractVariableFromExpression(node.callee); + return new Report(name ? [name] : undefined); + } + return Report.create(PASS_THROUGH); + }, + + alias (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TablePartial)) { + if (node.alias) { + return new Report(undefined, [new CompileError( + CompileErrorCode.UNEXPECTED_ALIAS, + 'A TablePartial shouldn\'t have an alias', + node.alias, + )]); + } + return new Report(extractVariableFromExpression(node.alias)); + } + if (isElementFieldNode(node, ElementKind.TablePartial)) { + return new Report(undefined); + } + return Report.create(PASS_THROUGH); + }, + + settings (compiler: Compiler, node: SyntaxNode): Report | Report { + if (isElementNode(node, ElementKind.TablePartial)) { + if (!node.attributeList) return new Report(new Map()); + + const settingsReport = collectSettings(node.attributeList); + const errors = settingsReport.getErrors(); + const settingMap = settingsReport.getValue(); + const clean: Settings = new Map(); + + for (const [name, attrs] of settingMap) { + switch (name) { + case SettingName.HeaderColor: + if (attrs.length > 1) { + errors.push(...attrs.map((attr: AttributeNode) => new CompileError(CompileErrorCode.DUPLICATE_TABLE_PARTIAL_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isValidColor(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_PARTIAL_SETTING_VALUE, `'${name}' must be a color literal`, attr.value || attr.name!)); + } + }); + clean.set(name, attrs); + break; + case SettingName.Note: + if (attrs.length > 1) { + errors.push(...attrs.map((attr: AttributeNode) => new CompileError(CompileErrorCode.DUPLICATE_TABLE_PARTIAL_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr: AttributeNode) => { + if (!isExpressionAQuotedString(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_TABLE_PARTIAL_SETTING_VALUE, `'${name}' must be a string literal`, attr.value || attr.name!)); + } + }); + clean.set(name, attrs); + break; + default: + errors.push(...attrs.map((attr: AttributeNode) => new CompileError(CompileErrorCode.UNKNOWN_TABLE_PARTIAL_SETTING, `Unknown '${name}' setting`, attr))); + } + } + + return new Report(clean, errors); + } + if (isElementFieldNode(node, ElementKind.TablePartial)) { + const errors: CompileError[] = []; + const map = new Map(); + + const remains = node.args.slice(1); + + const lastPart = last(remains); + if ( + (remains.length > 0 + && !remains.slice(0, -1).every(isExpressionAnIdentifierNode)) + || (lastPart && !(isExpressionAnIdentifierNode(lastPart) || lastPart instanceof ListExpressionNode)) + ) { + return new Report(new Map() as Settings, remains.map((part) => new CompileError(CompileErrorCode.INVALID_COLUMN, 'These fields must be some inline settings optionally ended with a setting list', part))); + } + + if (remains.length === 0) { + return new Report(map as Settings); + } + + let settingList: ListExpressionNode | undefined; + if (last(remains) instanceof ListExpressionNode) { + settingList = remains.pop() as ListExpressionNode; + } + + const settingsReport = collectSettings(settingList); + errors.push(...settingsReport.getErrors()); + const settingMap = settingsReport.getValue() as Map; + + remains.forEach((part) => { + const name = (extractVarNameFromPrimaryVariable(part as any) ?? '').toLowerCase(); + if (name !== SettingName.PK && name !== SettingName.Unique) { + errors.push(new CompileError(CompileErrorCode.INVALID_SETTINGS, 'Inline column settings can only be `pk` or `unique`', part)); + return; + } + if (!settingMap.has(name)) { + settingMap.set(name, [part as PrimaryExpressionNode]); + } else { + settingMap.get(name)!.push(part as PrimaryExpressionNode); + } + }); + + const pkAttrs = settingMap.get(SettingName.PK) || []; + const pkeyAttrs = settingMap.get(SettingName.PrimaryKey) || []; + if (pkAttrs.length >= 1 && pkeyAttrs.length >= 1) { + errors.push(...[...pkAttrs, ...pkeyAttrs].map((attr) => new CompileError( + CompileErrorCode.DUPLICATE_COLUMN_SETTING, + 'Either one of \'primary key\' and \'pk\' can appear', + attr, + ))); + } + + for (const [name, _attrs] of settingMap) { + const attrs = _attrs as AttributeNode[]; + switch (name) { + case SettingName.Note: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr) => { + if (!isExpressionAQuotedString(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must be a quoted string`, attr.value || attr.name!)); + } + }); + break; + + case SettingName.Ref: + attrs.forEach((attr) => { + if (!isUnaryRelationship(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must be a valid unary relationship`, attr.value || attr.name!)); + } + }); + break; + + case SettingName.PrimaryKey: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr) => { + if (!isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); + } + }); + break; + + case SettingName.PK: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr) => { + if (attr instanceof AttributeNode && !isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); + } + }); + break; + + case SettingName.NotNull: { + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); + } + const nullAttrs = settingMap.get(SettingName.Null) || []; + if (attrs.length >= 1 && nullAttrs.length >= 1) { + errors.push(...[...attrs, ...nullAttrs].map((attr) => new CompileError( + CompileErrorCode.CONFLICTING_SETTING, + '\'not null\' and \'null\' can not be set at the same time', + attr, + ))); + } + attrs.forEach((attr) => { + if (!isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); + } + }); + break; + } + + case SettingName.Null: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr) => { + if (!isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); + } + }); + break; + + case SettingName.Unique: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr) => { + if (attr instanceof AttributeNode && !isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); + } + }); + break; + + case SettingName.Increment: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr) => { + if (attr instanceof AttributeNode && !isVoid(attr.value)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, `'${name}' must not have a value`, attr.value || attr.name!)); + } + }); + break; + + case SettingName.Default: + if (attrs.length > 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_COLUMN_SETTING, `'${name}' can only appear once`, attr))); + } + attrs.forEach((attr) => { + if (!isValidDefaultValue(attr.value)) { + errors.push(new CompileError( + CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, + `'${name}' must be a string literal, number literal, function expression, true, false or null`, + attr.value || attr.name!, + )); + } + }); + break; + + case SettingName.Check: + attrs.forEach((attr) => { + if (!(attr.value instanceof FunctionExpressionNode)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_SETTING_VALUE, '\'check\' must be a function expression', attr.value || attr.name!)); + } + }); + break; + + default: + attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_COLUMN_SETTING, `Unknown column setting '${name}'`, attr))); + } + } + + for (const [name, attrs] of settingMap) { + map.set(name, attrs as AttributeNode[]); + } + + return new Report(map as Settings, errors); + } + return Report.create(PASS_THROUGH); + }, +}; diff --git a/packages/dbml-parse/src/core/syntax/local_modules/tablePartial/validate.ts b/packages/dbml-parse/src/core/syntax/local_modules/tablePartial/validate.ts new file mode 100644 index 000000000..73af2d7ea --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/tablePartial/validate.ts @@ -0,0 +1,86 @@ +import { isElementNode, isElementFieldNode } from '@/core/utils/expression'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { PASS_THROUGH, type PassThrough } from '@/constants'; +import { + BlockExpressionNode, + ElementDeclarationNode, + FunctionApplicationNode, + SyntaxNode, +} from '@/core/types/nodes'; +import { ElementKind } from '@/core/types/keywords'; +import { isValidColumnType } from '@/core/utils/validate'; +import { isExpressionAVariableNode } from '@/core/utils/expression'; +import Report from '@/core/types/report'; +import { partition } from 'lodash-es'; +import Compiler from '@/compiler'; + +export function validateTablePartial (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementNode(node, ElementKind.TablePartial)) { + return Report.create(PASS_THROUGH); + } + + const errors: CompileError[] = []; + + // Context: must be top-level + const parent = node.parentElement; + if (parent instanceof ElementDeclarationNode) { + errors.push(new CompileError( + CompileErrorCode.INVALID_TABLE_PARTIAL_CONTEXT, + 'TablePartial must appear top-level', + node, + )); + } + + // Collect errors from name, alias, settings validation + errors.push(...compiler.fullname(node).getErrors()); + errors.push(...compiler.alias(node).getErrors()); + errors.push(...compiler.settings(node).getErrors()); + + // Body validation + const body = node.body; + if (body instanceof FunctionApplicationNode) { + errors.push(new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A TablePartial\'s body must be a block', body)); + } else if (body instanceof BlockExpressionNode) { + const [fields, subs] = partition(body.body, (e) => e instanceof FunctionApplicationNode); + errors.push(...(fields as FunctionApplicationNode[]).flatMap((f) => compiler.validate(f).getErrors())); + + // Validate sub-elements + const subErrors = (subs as ElementDeclarationNode[]).flatMap((sub) => { + if (!sub.type) return []; + return compiler.validate(sub).getErrors(); + }); + errors.push(...subErrors); + + const notes = (subs as ElementDeclarationNode[]).filter((sub) => sub.type?.value.toLowerCase() === ElementKind.Note); + if (notes.length > 1) { + errors.push(...notes.map((note) => new CompileError(CompileErrorCode.NOTE_REDEFINED, 'Duplicate notes are defined', note))); + } + } + + return new Report(undefined, errors); +} + +export function validateTablePartialField (compiler: Compiler, node: SyntaxNode): Report | Report { + if (!isElementFieldNode(node, ElementKind.TablePartial)) { + return Report.create(PASS_THROUGH); + } + + if (!node.callee) return new Report(undefined); + + const errors: CompileError[] = []; + if (node.args.length === 0) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN, 'A column must have a type', node.callee!)); + } + + if (!isExpressionAVariableNode(node.callee)) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_NAME, 'A column name must be an identifier or a quoted identifier', node.callee)); + } + + if (node.args[0] && !isValidColumnType(node.args[0])) { + errors.push(new CompileError(CompileErrorCode.INVALID_COLUMN_TYPE, 'Invalid column type', node.args[0])); + } + + errors.push(...compiler.settings(node).getErrors()); + + return new Report(undefined, errors); +} diff --git a/packages/dbml-parse/src/core/syntax/local_modules/types.ts b/packages/dbml-parse/src/core/syntax/local_modules/types.ts new file mode 100644 index 000000000..ed2a7528e --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/local_modules/types.ts @@ -0,0 +1,20 @@ +import type Compiler from '@/compiler'; +import type { PassThrough } from '@/constants'; +import type { Module } from '@/core/types/module'; +import type { SyntaxNode } from '@/core/types/nodes'; +import type Report from '@/core/types/report'; +import type { Settings } from '@/core/utils/validate'; +export type { Settings } from '@/core/utils/validate'; + +// Local modules handle syntax-level queries (validation, naming, settings) for each DBML element kind. +// All methods are optional, missing methods are treated as returning PASS_THROUGH. +export interface LocalModule extends Module { + // Validate the syntax of this node, return errors in Report + validate? (compiler: Compiler, node: SyntaxNode): Report | Report; + // Extract the fully-qualified name segments (e.g. ['myschema', 'users']) + fullname? (compiler: Compiler, node: SyntaxNode): Report | Report; + // Extract the short alias (e.g. 'U' for Table users as U) + alias? (compiler: Compiler, node: SyntaxNode): Report | Report; + // Parse and validate the [bracket] settings, return clean settings with errors + settings? (compiler: Compiler, node: SyntaxNode): Report | Report; +} diff --git a/packages/dbml-parse/src/core/parser/contextStack.ts b/packages/dbml-parse/src/core/syntax/parser/contextStack.ts similarity index 98% rename from packages/dbml-parse/src/core/parser/contextStack.ts rename to packages/dbml-parse/src/core/syntax/parser/contextStack.ts index 00dc84d7b..45d6af4b5 100644 --- a/packages/dbml-parse/src/core/parser/contextStack.ts +++ b/packages/dbml-parse/src/core/syntax/parser/contextStack.ts @@ -1,5 +1,5 @@ import { last } from 'lodash-es'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; +import { SyntaxToken, SyntaxTokenKind } from '@/core/types/tokens'; export const enum ParsingContext { ListExpression, diff --git a/packages/dbml-parse/src/core/syntax/parser/factory.ts b/packages/dbml-parse/src/core/syntax/parser/factory.ts new file mode 100644 index 000000000..13b7f4162 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/parser/factory.ts @@ -0,0 +1,12 @@ +import { SyntaxNode, SyntaxNodeIdGenerator } from '@/core/types/nodes'; + +export default class NodeFactory { + constructor (generator: SyntaxNodeIdGenerator) { + // Set the static generator so SyntaxNode constructor uses this generator + SyntaxNode._idGenerator = generator; + } + + create(Type: { new (args: A): T }, args: A): T { + return new Type(args); + } +} diff --git a/packages/dbml-parse/src/core/parser/parser.ts b/packages/dbml-parse/src/core/syntax/parser/parser.ts similarity index 97% rename from packages/dbml-parse/src/core/parser/parser.ts rename to packages/dbml-parse/src/core/syntax/parser/parser.ts index 60d8c6251..df91347b2 100644 --- a/packages/dbml-parse/src/core/parser/parser.ts +++ b/packages/dbml-parse/src/core/syntax/parser/parser.ts @@ -1,21 +1,21 @@ import { last } from 'lodash-es'; import { convertFuncAppToElem, - isAsKeyword, + getMemberChain, markInvalid, -} from '@/core/parser/utils'; -import { CompileError, CompileErrorCode } from '@/core/errors'; -import { SyntaxToken, SyntaxTokenKind, isOpToken } from '@/core/lexer/tokens'; -import Report from '@/core/report'; -import { ParsingContext, ParsingContextStack } from '@/core/parser/contextStack'; +} from '@/core/syntax/parser/utils'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import { type SyntaxToken, SyntaxTokenKind, isOpToken } from '@/core/types/tokens'; +import Report from '@/core/types/report'; +import { ParsingContext, ParsingContextStack } from '@/core/syntax/parser/contextStack'; import { ArrayNode, AttributeNode, BlockExpressionNode, CallExpressionNode, CommaExpressionNode, - EmptyNode, ElementDeclarationNode, + EmptyNode, ExpressionNode, FunctionApplicationNode, FunctionExpressionNode, @@ -30,12 +30,13 @@ import { PrimaryExpressionNode, ProgramNode, SyntaxNode, - SyntaxNodeIdGenerator, TupleExpressionNode, VariableNode, -} from '@/core/parser/nodes'; -import NodeFactory from '@/core/parser/factory'; -import { hasTrailingNewLines, hasTrailingSpaces, isAtStartOfLine } from '@/core/lexer/utils'; + SyntaxNodeIdGenerator, +} from '@/core/types/nodes'; +import NodeFactory from '@/core/syntax/parser/factory'; +import { hasTrailingNewLines, hasTrailingSpaces, isAtStartOfLine } from '@/core/syntax/lexer/utils'; +import { isAsKeyword } from '@/core/utils/expression'; // A class of errors that represent a parsing failure and contain the node that was partially parsed class PartialParsingError { @@ -178,10 +179,21 @@ export default class Parser { const eof = this.advance(); const program = this.nodeFactory.create(ProgramNode, { body, eof, source: this.source }); this.gatherInvalid(); + this.assignParents(program); return new Report({ ast: program, tokens: this.tokens }, this.errors); } + // Visit all nodes in the program and assign their parent + private assignParents (node: SyntaxNode) { + getMemberChain(node).forEach((child) => { + if (child instanceof SyntaxNode) { + child.parent = node; + this.assignParents(child); + } + }); + } + /* Parsing and synchronizing ProgramNode */ private program () { @@ -407,9 +419,8 @@ export default class Parser { // Try interpreting the function application as an element declaration expression // if fail, fall back to the generic function application - const buildExpression = () => convertFuncAppToElem(args.callee, args.args, this.nodeFactory).unwrap_or( - this.nodeFactory.create(FunctionApplicationNode, args), - ); + const buildExpression = () => convertFuncAppToElem(args.callee, args.args, this.nodeFactory) + ?? this.nodeFactory.create(FunctionApplicationNode, args); try { args.callee = this.commaExpression(); diff --git a/packages/dbml-parse/src/core/parser/utils.ts b/packages/dbml-parse/src/core/syntax/parser/utils.ts similarity index 61% rename from packages/dbml-parse/src/core/parser/utils.ts rename to packages/dbml-parse/src/core/syntax/parser/utils.ts index 70b179874..d6421180b 100644 --- a/packages/dbml-parse/src/core/parser/utils.ts +++ b/packages/dbml-parse/src/core/syntax/parser/utils.ts @@ -1,8 +1,5 @@ import { last } from 'lodash-es'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; -import { None, Option, Some } from '@/core/option'; -import { alternateLists } from '@/core/utils'; -import NodeFactory from '@/core/parser/factory'; +import NodeFactory from '@/core/syntax/parser/factory'; import { ArrayNode, AttributeNode, @@ -27,15 +24,18 @@ import { SyntaxNode, TupleExpressionNode, VariableNode, -} from '@/core/parser/nodes'; -import { destructureComplexVariable } from '@/core/analyzer/utils'; +} from '@/core/types/nodes'; +import { extractVariableNode, isAsKeyword, isExpressionAnIdentifierNode } from '@/core/utils/expression'; +import { SyntaxToken } from '@/core/types/tokens'; +import { SyntaxTokenKind } from '@/index'; +import { alternateLists } from '@/core/utils/chars'; // Try to interpret a function application as an element export function convertFuncAppToElem ( _callee: ExpressionNode | CommaExpressionNode | undefined, _args: (NormalExpressionNode | CommaExpressionNode)[], factory: NodeFactory, -): Option { +): ElementDeclarationNode | undefined { let args = _args; let callee = _callee; // Handle the case: @@ -47,15 +47,15 @@ export function convertFuncAppToElem ( callee = callee.callee; } if (!callee || !isExpressionAnIdentifierNode(callee) || args.length === 0) { - return new None(); + return undefined; } const cpArgs = [...args]; - const type = extractVariableNode(callee).unwrap(); + const type = extractVariableNode(callee)!; const body = cpArgs.pop(); if (!(body instanceof BlockExpressionNode)) { - return new None(); + return undefined; } const attributeList = last(cpArgs) instanceof ListExpressionNode @@ -63,51 +63,38 @@ export function convertFuncAppToElem ( : undefined; if (cpArgs.length === 3) { - const asKeywordNode = extractVariableNode(cpArgs[1]).value; + const asKeywordNode = extractVariableNode(cpArgs[1]); // If cpArgs = [sth, 'as', sth] then it's a valid element declaration return (!asKeywordNode || !isAsKeyword(asKeywordNode)) - ? new None() - : new Some( - factory.create(ElementDeclarationNode, { - type, - name: cpArgs[0], - as: asKeywordNode, - alias: cpArgs[2], - attributeList, - body, - }), - ); + ? undefined + : factory.create(ElementDeclarationNode, { + type, + name: cpArgs[0], + as: asKeywordNode, + alias: cpArgs[2], + attributeList, + body, + }); } if (cpArgs.length === 1) { - return new Some( - factory.create(ElementDeclarationNode, { - type, - name: cpArgs[0], - attributeList, - body, - }), - ); + return factory.create(ElementDeclarationNode, { + type, + name: cpArgs[0], + attributeList, + body, + }); } if (cpArgs.length === 0) { - return new Some( - factory.create(ElementDeclarationNode, { - type, - attributeList, - body, - }), - ); + return factory.create(ElementDeclarationNode, { + type, + attributeList, + body, + }); } - return new None(); -} - -// Check if a token is an `as` keyword -export function isAsKeyword ( - token?: SyntaxToken, -): token is SyntaxToken & { kind: SyntaxTokenKind.IDENTIFIER; value: 'as' } { - return token?.kind === SyntaxTokenKind.IDENTIFIER && token.value.toLowerCase() === 'as'; + return undefined; } export function markInvalid (nodeOrToken?: SyntaxNode | SyntaxToken) { @@ -309,105 +296,9 @@ export function getMemberChain (node: SyntaxNode): Readonly<(SyntaxNode | Syntax throw new Error('This case is already handled by TupleExpressionNode'); } - throw new Error('Unreachable - no other possible cases'); -} - -// Return a variable node if it's nested inside a primary expression -export function extractVariableNode (value?: unknown): Option { - if (isExpressionAVariableNode(value)) { - return new Some(value.expression.variable); + if (node instanceof EmptyNode) { + return []; } - return new None(); -} - -// Return true if an expression node is a primary expression -// with a nested quoted string (", ' or ''') -export function isExpressionAQuotedString (value?: unknown): value is PrimaryExpressionNode - & ( - | { expression: VariableNode & { variable: SyntaxToken & { kind: SyntaxTokenKind.QUOTED_STRING } } } - | { - expression: LiteralNode & { - literal: SyntaxToken & { kind: SyntaxTokenKind.STRING_LITERAL }; - }; - } - ) { - return ( - value instanceof PrimaryExpressionNode - && ( - ( - value.expression instanceof VariableNode - && value.expression.variable instanceof SyntaxToken - && value.expression.variable.kind === SyntaxTokenKind.QUOTED_STRING - ) - || ( - value.expression instanceof LiteralNode - && value.expression.literal?.kind === SyntaxTokenKind.STRING_LITERAL - ) - ) - ); -} - -// Return true if an expression node is a primary expression -// with a variable node (identifier or a double-quoted string) -export function isExpressionAVariableNode ( - value?: unknown, -): value is PrimaryExpressionNode & { expression: VariableNode & { variable: SyntaxToken } } { - return ( - value instanceof PrimaryExpressionNode - && value.expression instanceof VariableNode - && value.expression.variable instanceof SyntaxToken - ); -} - -// Return true if an expression node is a primary expression -// with an identifier-like variable node -export function isExpressionAnIdentifierNode (value?: unknown): value is PrimaryExpressionNode & { - expression: VariableNode & { variable: { kind: SyntaxTokenKind.IDENTIFIER } }; -} { - return ( - value instanceof PrimaryExpressionNode - && value.expression instanceof VariableNode - && value.expression.variable?.kind === SyntaxTokenKind.IDENTIFIER - ); -} - -type AccessExpression = InfixExpressionNode & { - leftExpression: SyntaxNode; - rightExpression: SyntaxNode; - op: SyntaxToken & { value: '.' }; -}; - -type DotDelimitedIdentifier = PrimaryExpressionNode | (AccessExpression & { - rightExpression: AccessExpression | PrimaryExpressionNode; -}); - -export function isAccessExpression (node?: SyntaxNode): node is AccessExpression { - return ( - node instanceof InfixExpressionNode - && node.leftExpression instanceof SyntaxNode - && node.rightExpression instanceof SyntaxNode - && node.op?.value === '.' - ); -} - -export function isDotDelimitedIdentifier (node?: SyntaxNode): node is DotDelimitedIdentifier { - if (isExpressionAVariableNode(node)) return true; - return isAccessExpression(node) && isExpressionAVariableNode(node.rightExpression) && isDotDelimitedIdentifier(node.leftExpression); -} - -export function extractStringFromIdentifierStream (stream?: IdentiferStreamNode): Option { - if (stream === undefined) { - return new None(); - } - const name = stream.identifiers.map((identifier) => identifier.value).join(' '); - if (name === '') { - return new None(); - } - - return new Some(name); -} - -export function getElementNameString (element?: ElementDeclarationNode): Option { - return destructureComplexVariable(element?.name).map((ss) => ss.join('.')); + throw new Error('Unreachable - no other possible cases'); } diff --git a/packages/dbml-parse/src/core/syntax/utils.ts b/packages/dbml-parse/src/core/syntax/utils.ts new file mode 100644 index 000000000..108a26a42 --- /dev/null +++ b/packages/dbml-parse/src/core/syntax/utils.ts @@ -0,0 +1,181 @@ +import { last } from 'lodash-es'; +import { + FunctionExpressionNode, + InfixExpressionNode, + LiteralNode, + PrimaryExpressionNode, + SyntaxNode, + TupleExpressionNode, + VariableNode, + CallExpressionNode, +} from '@/core/types/nodes'; +import { SyntaxTokenKind } from '@/core/types/tokens'; +import { isAccessExpression, isExpressionAQuotedString, isExpressionAVariableNode, isRelationshipOp, isTupleOfVariables } from '../utils/expression'; + +export function destructureMemberAccessExpression (node?: SyntaxNode): SyntaxNode[] | undefined { + if (!node) return undefined; + + if (!isAccessExpression(node)) { + return [node]; + } + + const fragments = destructureMemberAccessExpression(node.leftExpression); + if (!fragments) return undefined; + + fragments.push(node.rightExpression); + return fragments; +} + +export function destructureComplexVariable (node?: SyntaxNode): string[] | undefined { + if (node === undefined) return undefined; + + const fragments = destructureMemberAccessExpression(node); + if (!fragments) return undefined; + + const variables: string[] = []; + for (const fragment of fragments) { + const variable = extractVariableFromExpression(fragment); + if (typeof variable !== 'string') return undefined; + variables.push(variable); + } + + return variables; +} + +export function destructureComplexVariableTuple ( + node?: SyntaxNode, +): { variables: (PrimaryExpressionNode & { expression: VariableNode })[]; tupleElements: (PrimaryExpressionNode & { expression: VariableNode })[] } | undefined { + if (node === undefined) return undefined; + + const fragments = destructureMemberAccessExpression(node); + if (!fragments || fragments.length === 0) return undefined; + + let tupleElements: (PrimaryExpressionNode & { expression: VariableNode })[] = []; + + if (!isExpressionAVariableNode(last(fragments))) { + const topFragment = fragments.pop()!; + if (isTupleOfVariables(topFragment)) { + tupleElements = topFragment.elementList; + } else { + return undefined; + } + } + + const variables = fragments; + if (!variables.every(isExpressionAVariableNode)) return undefined; + + return { variables, tupleElements }; +} + +export function extractVariableFromExpression (node?: SyntaxNode): string | undefined { + if (!isExpressionAVariableNode(node)) return undefined; + return node.expression.variable.value; +} + +export function destructureIndexNode (node?: SyntaxNode): { + functional: FunctionExpressionNode[]; + nonFunctional: (PrimaryExpressionNode & { expression: VariableNode })[]; +} | undefined { + if (isValidIndexName(node)) { + return node instanceof FunctionExpressionNode + ? { functional: [node], nonFunctional: [] } + : { functional: [], nonFunctional: [node] }; + } + + if (node instanceof TupleExpressionNode && node.elementList.every(isValidIndexName)) { + const functionalIndexName = node.elementList.filter( + (e) => e instanceof FunctionExpressionNode, + ) as FunctionExpressionNode[]; + const nonfunctionalIndexName = node.elementList.filter(isExpressionAVariableNode); + return { functional: functionalIndexName, nonFunctional: nonfunctionalIndexName }; + } + + return undefined; +} + +export function extractVarNameFromPrimaryVariable ( + node?: PrimaryExpressionNode & { expression: VariableNode }, +): string | undefined { + return node?.expression.variable?.value; +} + +export function extractQuotedStringToken (value?: SyntaxNode): string | undefined { + if (!isExpressionAQuotedString(value)) return undefined; + + if (value?.expression instanceof VariableNode) { + return value?.expression?.variable?.value; + } + + return value.expression.literal.value; +} + +export function extractNumericLiteral (node?: SyntaxNode): number | null { + if (node instanceof PrimaryExpressionNode && node.expression instanceof LiteralNode) { + if (node.expression.literal?.kind === SyntaxTokenKind.NUMERIC_LITERAL) { + return Number(node.expression.literal.value); + } + } + return null; +} + +function isValidRefEndpoint (node?: SyntaxNode): boolean { + if (!node) return false; + // Simple dotted chain: a.b.c + if (destructureComplexVariableTuple(node)) return true; + // Standalone tuple of dotted chains: (a.b, c.d) + if (node instanceof TupleExpressionNode) { + return node.elementList.length > 0 && node.elementList.every((e) => destructureComplexVariable(e) !== undefined); + } + return false; +} + +export function isBinaryRelationship (value?: SyntaxNode): value is InfixExpressionNode { + if (!(value instanceof InfixExpressionNode)) return false; + if (!isRelationshipOp(value.op?.value)) return false; + + return isValidRefEndpoint(value.leftExpression) && isValidRefEndpoint(value.rightExpression); +} + +function countEndpointColumns (node?: SyntaxNode): number { + if (!node) return 0; + const tuple = destructureComplexVariableTuple(node); + if (tuple) return Math.max(1, tuple.tupleElements.length); + if (node instanceof TupleExpressionNode) return node.elementList.length; + return 0; +} + +export function isEqualTupleOperands (value: InfixExpressionNode): boolean { + return countEndpointColumns(value.leftExpression) === countEndpointColumns(value.rightExpression); +} + +export function isValidIndexName ( + value?: SyntaxNode, +): value is (PrimaryExpressionNode & { expression: VariableNode }) | FunctionExpressionNode { + return ( + (value instanceof PrimaryExpressionNode && value.expression instanceof VariableNode) + || value instanceof FunctionExpressionNode + ); +} + +export function destructureCallExpression ( + node?: SyntaxNode, +): { + variables: (PrimaryExpressionNode & { expression: VariableNode })[]; + args: (PrimaryExpressionNode & { expression: VariableNode })[]; +} | undefined { + if (!(node instanceof CallExpressionNode) || !node.callee) return undefined; + + const fragments = destructureMemberAccessExpression(node.callee); + if (!fragments || fragments.length === 0) return undefined; + if (!fragments.every(isExpressionAVariableNode)) return undefined; + + let args: (PrimaryExpressionNode & { expression: VariableNode })[] = []; + if (isTupleOfVariables(node.argumentList)) { + args = [...node.argumentList.elementList]; + } + + return { + variables: fragments as (PrimaryExpressionNode & { expression: VariableNode })[], + args, + }; +} diff --git a/packages/dbml-parse/src/core/errors.ts b/packages/dbml-parse/src/core/types/errors.ts similarity index 97% rename from packages/dbml-parse/src/core/errors.ts rename to packages/dbml-parse/src/core/types/errors.ts index d453b7e71..1abd52df0 100644 --- a/packages/dbml-parse/src/core/errors.ts +++ b/packages/dbml-parse/src/core/types/errors.ts @@ -1,5 +1,5 @@ -import { SyntaxToken } from './lexer/tokens'; -import { SyntaxNode } from './parser/nodes'; +import { SyntaxToken } from '@/core/types/tokens'; +import { SyntaxNode } from './nodes'; export enum CompileErrorCode { UNKNOWN_SYMBOL = 1000, diff --git a/packages/dbml-parse/src/core/types/internable.ts b/packages/dbml-parse/src/core/types/internable.ts new file mode 100644 index 000000000..1c76abbb5 --- /dev/null +++ b/packages/dbml-parse/src/core/types/internable.ts @@ -0,0 +1,65 @@ +// An entity that can be interned , i.e. reduced to a stable, opaque identity +// suitable for use as a map key or cache key. +// Id must be a primitive type so it can serve as a reliable Map key. +export interface Internable { + intern (): Id; +} + +export type Primitive = string | number | boolean | symbol | bigint | null | undefined; + +export function intern (value: Internable | T | T[]): Id | T | string; +export function intern (value: Internable): Id; +export function intern (value: T): T; +export function intern (value: T[]): string; +export function intern (value: Primitive | Primitive[] | Internable): unknown { + if (value === null || value === undefined) return value; + if (Array.isArray(value)) return value.map(String).join('\0'); + if (typeof value === 'object' && 'intern' in value) return value.intern(); + return value; +} + +// A Map keyed by Internable objects. Automatically interns keys on every +// access so callers never need to call .intern() themselves. +export class InternedMap, V, P extends Primitive = ReturnType> { + private readonly map: Map; + + constructor () { + this.map = new Map(); + } + + get (key: K): V | undefined { + return this.map.get(key.intern() as P); + } + + set (key: K, value: V): this { + this.map.set(key.intern() as P, value); + return this; + } + + has (key: K): boolean { + return this.map.has(key.intern() as P); + } + + delete (key: K): boolean { + return this.map.delete(key.intern() as P); + } + + get size (): number { + return this.map.size; + } + + entries (): IterableIterator<[P, V]> { + return this.map[Symbol.iterator](); + } + + [Symbol.iterator] (): IterableIterator<[P, V]> { + return this.map[Symbol.iterator](); + } + + merge (other: InternedMap): this { + for (const [k, v] of other.map) { + this.map.set(k, v); + } + return this; + } +} diff --git a/packages/dbml-parse/src/core/analyzer/types.ts b/packages/dbml-parse/src/core/types/keywords.ts similarity index 96% rename from packages/dbml-parse/src/core/analyzer/types.ts rename to packages/dbml-parse/src/core/types/keywords.ts index 587dbbdcc..37fab649c 100644 --- a/packages/dbml-parse/src/core/analyzer/types.ts +++ b/packages/dbml-parse/src/core/types/keywords.ts @@ -7,7 +7,7 @@ export enum ElementKind { Indexes = 'indexes', TableGroup = 'tablegroup', TablePartial = 'tablepartial', - Check = 'checks', + Checks = 'checks', Records = 'records', } diff --git a/packages/dbml-parse/src/core/types/module.ts b/packages/dbml-parse/src/core/types/module.ts new file mode 100644 index 000000000..605118277 --- /dev/null +++ b/packages/dbml-parse/src/core/types/module.ts @@ -0,0 +1,7 @@ +import type { PassThrough } from '@/constants'; +import type Report from './report'; +import type Compiler from '@/compiler'; + +export interface Module { + [index: string]: undefined | ((compiler: Compiler, ...args: any[]) => Report | Report); +} diff --git a/packages/dbml-parse/src/core/parser/nodes.ts b/packages/dbml-parse/src/core/types/nodes.ts similarity index 77% rename from packages/dbml-parse/src/core/parser/nodes.ts rename to packages/dbml-parse/src/core/types/nodes.ts index d842734ca..2e6e95dd9 100644 --- a/packages/dbml-parse/src/core/parser/nodes.ts +++ b/packages/dbml-parse/src/core/types/nodes.ts @@ -1,10 +1,16 @@ import { flatten, zip } from 'lodash-es'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; -import { NodeSymbol } from '@/core/analyzer/symbol/symbols'; -import { Position } from '@/core/types'; -import { getTokenFullEnd, getTokenFullStart } from '@/core/lexer/utils'; +import type { Position } from '@/core/types/position'; +import { getTokenFullEnd, getTokenFullStart } from '@/core/syntax/lexer/utils'; +import type { Internable } from '@/core/types/internable'; +import { SyntaxToken } from './tokens'; +import { type ElementKind, SyntaxTokenKind } from '@/index'; + +declare const __syntaxNodeIdBrand: unique symbol; +export type SyntaxNodeId = number & { readonly [__syntaxNodeIdBrand]: true }; + +declare const __internedSyntaxNodeBrand: unique symbol; +export type InternedSyntaxNode = string & { readonly [__internedSyntaxNodeBrand]: true }; -export type SyntaxNodeId = number; export class SyntaxNodeIdGenerator { private id = 0; @@ -13,12 +19,16 @@ export class SyntaxNodeIdGenerator { } nextId (): SyntaxNodeId { - return this.id++; + return this.id++ as SyntaxNodeId; } } -export class SyntaxNode { +export class SyntaxNode implements Internable { + /* @internal only accessed in testcase */ + static _idGenerator = new SyntaxNodeIdGenerator(); + id: Readonly; + kind: SyntaxNodeKind; startPos: Readonly; start: Readonly; @@ -26,16 +36,14 @@ export class SyntaxNode { endPos: Readonly; end: Readonly; fullEnd: Readonly; // End offset with trivias counted - symbol?: NodeSymbol; - referee?: NodeSymbol; // The symbol that this syntax node refers to + parent?: SyntaxNode; // args must be passed in order of appearance in the node constructor ( - id: SyntaxNodeId, kind: SyntaxNodeKind, args: Readonly[], ) { - this.id = id; + this.id = SyntaxNode._idGenerator.nextId(); this.kind = kind; const firstValid = args.find((sub) => sub !== undefined && !Number.isNaN(sub.start)); @@ -73,9 +81,51 @@ export class SyntaxNode { this.start = this.startPos.offset; this.end = this.endPos.offset; } + + intern (): InternedSyntaxNode { + return `node@${this.id}` as InternedSyntaxNode; + } + + // Walk up the tree to find the nearest enclosing element or program + get parentElement (): ElementDeclarationNode | ProgramNode | undefined { + let current: SyntaxNode | undefined = this.parent; + while (current) { + if (current instanceof ElementDeclarationNode || current instanceof ProgramNode) { + return current; + } + current = current.parent; + } + return undefined; + } + + parentOfKind (cls: (new (...args: any[]) => T)): T | undefined { + let current: SyntaxNode | undefined = this.parent; + while (current) { + if (current instanceof cls) { + return current; + } + current = current.parent; + } + return undefined; + } + + // Return if `otherNode` is strictly contained inside this node + strictlyContains (otherNode: SyntaxNode): boolean { + const thisSmallerStart = this.start < otherNode.start; + const thisSmallerEqStart = thisSmallerStart || this.start === otherNode.start; + const thisGreaterEnd = this.end > otherNode.end; + const thisGreaterEqEnd = thisGreaterEnd || this.end === otherNode.end; + return (thisSmallerStart && thisGreaterEqEnd) || (thisSmallerEqStart && thisGreaterEnd); + } + + // Return if `otherNode` is contained inside this node or equals this node + containsEq (otherNode: SyntaxNode): boolean { + return this.start <= otherNode.start + && this.end >= otherNode.end; + } } -export enum SyntaxNodeKind { +export const enum SyntaxNodeKind { PROGRAM = '', ELEMENT_DECLARATION = '', ATTRIBUTE = '', @@ -114,9 +164,8 @@ export class ProgramNode extends SyntaxNode { constructor ( { body = [], eof, source }: { body?: ElementDeclarationNode[]; eof?: SyntaxToken; source: string }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.PROGRAM, [...body, eof]); + super(SyntaxNodeKind.PROGRAM, [...body, eof]); this.source = source; this.body = body; this.eof = eof; @@ -140,8 +189,6 @@ export class ElementDeclarationNode extends SyntaxNode { bodyColon?: SyntaxToken; - parent?: ElementDeclarationNode | ProgramNode; // The enclosing element/program - body?: FunctionApplicationNode | BlockExpressionNode; constructor ( @@ -162,9 +209,8 @@ export class ElementDeclarationNode extends SyntaxNode { bodyColon?: SyntaxToken; body?: BlockExpressionNode | FunctionApplicationNode; }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.ELEMENT_DECLARATION, [ + super(SyntaxNodeKind.ELEMENT_DECLARATION, [ type, name, as, @@ -189,6 +235,10 @@ export class ElementDeclarationNode extends SyntaxNode { this.bodyColon = bodyColon; this.body = body; } + + isKind (...kinds: ElementKind[]): boolean { + return this.type?.value !== undefined && kinds.map((k) => k.toLowerCase()).includes(this.type.value.toLowerCase()); + } } // Form: * @@ -198,8 +248,8 @@ export class ElementDeclarationNode extends SyntaxNode { export class IdentiferStreamNode extends SyntaxNode { identifiers: SyntaxToken[]; - constructor ({ identifiers = [] }: { identifiers?: SyntaxToken[] }, id: SyntaxNodeId) { - super(id, SyntaxNodeKind.IDENTIFIER_STREAM, identifiers || []); + constructor ({ identifiers = [] }: { identifiers?: SyntaxToken[] }) { + super(SyntaxNodeKind.IDENTIFIER_STREAM, identifiers || []); this.identifiers = identifiers; } } @@ -226,9 +276,8 @@ export class AttributeNode extends SyntaxNode { colon?: SyntaxToken; value?: NormalExpressionNode | IdentiferStreamNode; }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.ATTRIBUTE, [name, colon, value]); + super(SyntaxNodeKind.ATTRIBUTE, [name, colon, value]); this.name = name; this.value = value; this.colon = colon; @@ -268,9 +317,8 @@ export class PrefixExpressionNode extends SyntaxNode { constructor ( { op, expression }: { op?: SyntaxToken; expression?: NormalExpressionNode }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.PREFIX_EXPRESSION, [op, expression]); + super(SyntaxNodeKind.PREFIX_EXPRESSION, [op, expression]); this.op = op; this.expression = expression; } @@ -298,9 +346,8 @@ export class InfixExpressionNode extends SyntaxNode { leftExpression?: NormalExpressionNode; rightExpression?: NormalExpressionNode; }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.INFIX_EXPRESSION, [leftExpression, op, rightExpression]); + super(SyntaxNodeKind.INFIX_EXPRESSION, [leftExpression, op, rightExpression]); this.op = op; this.leftExpression = leftExpression; this.rightExpression = rightExpression; @@ -317,9 +364,8 @@ export class PostfixExpressionNode extends SyntaxNode { constructor ( { op, expression }: { op?: SyntaxToken; expression?: NormalExpressionNode }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.POSTFIX_EXPRESSION, [expression, op]); + super(SyntaxNodeKind.POSTFIX_EXPRESSION, [expression, op]); this.op = op; this.expression = expression; } @@ -332,8 +378,8 @@ export class PostfixExpressionNode extends SyntaxNode { export class FunctionExpressionNode extends SyntaxNode { value?: SyntaxToken; - constructor ({ value }: { value?: SyntaxToken }, id: SyntaxNodeId) { - super(id, SyntaxNodeKind.FUNCTION_EXPRESSION, [value]); + constructor ({ value }: { value?: SyntaxToken }) { + super(SyntaxNodeKind.FUNCTION_EXPRESSION, [value]); this.value = value; } } @@ -350,9 +396,8 @@ export class FunctionApplicationNode extends SyntaxNode { constructor ( { callee, args = [] }: { callee?: ExpressionNode; args?: ExpressionNode[] }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.FUNCTION_APPLICATION, [callee, ...args]); + super(SyntaxNodeKind.FUNCTION_APPLICATION, [callee, ...args]); this.callee = callee; this.args = args; } @@ -379,9 +424,8 @@ export class BlockExpressionNode extends SyntaxNode { body?: (ElementDeclarationNode | FunctionApplicationNode)[]; blockCloseBrace?: SyntaxToken; }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.BLOCK_EXPRESSION, [blockOpenBrace, ...body, blockCloseBrace]); + super(SyntaxNodeKind.BLOCK_EXPRESSION, [blockOpenBrace, ...body, blockCloseBrace]); this.blockOpenBrace = blockOpenBrace; this.body = body; this.blockCloseBrace = blockCloseBrace; @@ -413,9 +457,8 @@ export class ListExpressionNode extends SyntaxNode { commaList?: SyntaxToken[]; listCloseBracket?: SyntaxToken; }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.LIST_EXPRESSION, [ + super(SyntaxNodeKind.LIST_EXPRESSION, [ listOpenBracket, ...interleave(elementList, commaList), listCloseBracket, @@ -452,9 +495,8 @@ export class TupleExpressionNode extends SyntaxNode { commaList?: SyntaxToken[]; tupleCloseParen?: SyntaxToken; }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.TUPLE_EXPRESSION, [ + super(SyntaxNodeKind.TUPLE_EXPRESSION, [ tupleOpenParen, ...interleave(elementList, commaList), tupleCloseParen, @@ -487,9 +529,8 @@ export class CommaExpressionNode extends SyntaxNode { elementList?: NormalExpressionNode[]; commaList?: SyntaxToken[]; }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.COMMA_EXPRESSION, [ + super(SyntaxNodeKind.COMMA_EXPRESSION, [ ...interleave(elementList, commaList), ]); this.elementList = elementList; @@ -512,17 +553,13 @@ export class GroupExpressionNode extends TupleExpressionNode { expression?: NormalExpressionNode; groupCloseParen?: SyntaxToken; }, - id: SyntaxNodeId, ) { - super( - { - tupleOpenParen: groupOpenParen, - elementList: expression && [expression], - commaList: [], - tupleCloseParen: groupCloseParen, - }, - id, - ); + super({ + tupleOpenParen: groupOpenParen, + elementList: expression && [expression], + commaList: [], + tupleCloseParen: groupCloseParen, + }); this.kind = SyntaxNodeKind.GROUP_EXPRESSION; } } @@ -544,9 +581,8 @@ export class CallExpressionNode extends SyntaxNode { callee?: NormalExpressionNode; argumentList?: TupleExpressionNode; }, - id: SyntaxNodeId, ) { - super(id, SyntaxNodeKind.CALL_EXPRESSION, [callee, argumentList]); + super(SyntaxNodeKind.CALL_EXPRESSION, [callee, argumentList]); this.callee = callee; this.argumentList = argumentList; } @@ -560,8 +596,8 @@ export class CallExpressionNode extends SyntaxNode { export class LiteralNode extends SyntaxNode { literal?: SyntaxToken; - constructor ({ literal }: { literal?: SyntaxToken }, id: SyntaxNodeId) { - super(id, SyntaxNodeKind.LITERAL, [literal]); + constructor ({ literal }: { literal?: SyntaxToken }) { + super(SyntaxNodeKind.LITERAL, [literal]); this.literal = literal; } } @@ -573,8 +609,8 @@ export class LiteralNode extends SyntaxNode { export class VariableNode extends SyntaxNode { variable?: SyntaxToken; - constructor ({ variable }: { variable?: SyntaxToken }, id: SyntaxNodeId) { - super(id, SyntaxNodeKind.VARIABLE, [variable]); + constructor ({ variable }: { variable?: SyntaxToken }) { + super(SyntaxNodeKind.VARIABLE, [variable]); this.variable = variable; } } @@ -586,8 +622,8 @@ export class VariableNode extends SyntaxNode { export class PrimaryExpressionNode extends SyntaxNode { expression?: LiteralNode | VariableNode; - constructor ({ expression }: { expression?: LiteralNode | VariableNode }, id: SyntaxNodeId) { - super(id, SyntaxNodeKind.PRIMARY_EXPRESSION, [expression]); + constructor ({ expression }: { expression?: LiteralNode | VariableNode }) { + super(SyntaxNodeKind.PRIMARY_EXPRESSION, [expression]); this.expression = expression; } } @@ -598,9 +634,9 @@ export class PrimaryExpressionNode extends SyntaxNode { // - Empty fields in comma expressions (e.g. 1, , 3) // - Trailing commas in comma expressions (e.g. 1, 2,) export class EmptyNode extends SyntaxNode { - constructor ({ prevToken }: { prevToken: Readonly | Readonly }, id: SyntaxNodeId) { + constructor ({ prevToken }: { prevToken: Readonly | Readonly }) { const nextToken = SyntaxToken.create(SyntaxTokenKind.SPACE, prevToken.endPos, prevToken.endPos, ' ', false); - super(id, SyntaxNodeKind.EMPTY, [nextToken]); + super(SyntaxNodeKind.EMPTY, [nextToken]); } } @@ -612,8 +648,8 @@ export class ArrayNode extends SyntaxNode { array?: NormalExpressionNode; indexer?: ListExpressionNode; - constructor ({ expression, indexer }: { expression?: NormalExpressionNode; indexer: ListExpressionNode }, id: SyntaxNodeId) { - super(id, SyntaxNodeKind.ARRAY, [expression, indexer]); + constructor ({ expression, indexer }: { expression?: NormalExpressionNode; indexer: ListExpressionNode }) { + super(SyntaxNodeKind.ARRAY, [expression, indexer]); this.array = expression; this.indexer = indexer; } diff --git a/packages/dbml-parse/src/core/types.ts b/packages/dbml-parse/src/core/types/position.ts similarity index 100% rename from packages/dbml-parse/src/core/types.ts rename to packages/dbml-parse/src/core/types/position.ts diff --git a/packages/dbml-parse/src/core/report.ts b/packages/dbml-parse/src/core/types/report.ts similarity index 82% rename from packages/dbml-parse/src/core/report.ts rename to packages/dbml-parse/src/core/types/report.ts index 33c252e48..316058c30 100644 --- a/packages/dbml-parse/src/core/report.ts +++ b/packages/dbml-parse/src/core/types/report.ts @@ -8,6 +8,10 @@ export default class Report { private warnings?: CompileWarning[]; + static create (value: T, errors?: CompileError[], warnings?: CompileWarning[]) { + return new Report(value, errors, warnings); + } + constructor (value: T, errors?: CompileError[], warnings?: CompileWarning[]) { this.value = value; this.errors = errors === undefined ? [] : errors; @@ -16,6 +20,10 @@ export default class Report { } } + hasValue (value: S): this is Report { + return this.value as any === value; + } + getValue (): T { return this.value; } diff --git a/packages/dbml-parse/src/core/types/schemaJson.ts b/packages/dbml-parse/src/core/types/schemaJson.ts new file mode 100644 index 000000000..36dff9018 --- /dev/null +++ b/packages/dbml-parse/src/core/types/schemaJson.ts @@ -0,0 +1,493 @@ +import type { Position } from './position'; + +export interface TokenPosition { + start: Position; + end: Position; +} + +export class SchemaElement { + token: TokenPosition; + + constructor (token: TokenPosition) { + this.token = token; + } +} + +// Create a class instance from a plain object (bypasses constructor, sets prototype) +export function schemaFrom (ctor: abstract new (...args: any[]) => T, init: Omit): T { + const instance = Object.create(ctor.prototype) as T; + return Object.assign(instance, init); +} + +export class Database extends SchemaElement { + schemas: [] = []; + tables: Table[] = []; + notes: Note[] = []; + refs: Ref[] = []; + enums: Enum[] = []; + tableGroups: TableGroup[] = []; + aliases: Alias[] = []; + project?: Project; + tablePartials: TablePartial[] = []; + records: TableRecord[] = []; +} + +export class Table extends SchemaElement { + name: string; + schemaName: string | null; + alias: string | null; + fields: Column[] = []; + checks: Check[] = []; + partials: TablePartialInjection[] = []; + indexes: Index[] = []; + headerColor?: string; + note?: { value: string; token: TokenPosition }; + + constructor (token: TokenPosition, name: string, schemaName: string | null, alias: string | null) { + super(token); + this.name = name; + this.schemaName = schemaName; + this.alias = alias; + } + + toJSON () { + return { + name: this.name, + schemaName: this.schemaName, + alias: this.alias, + fields: this.fields, + token: this.token, + indexes: this.indexes, + partials: this.partials, + checks: this.checks, + ...(this.headerColor !== undefined && { headerColor: this.headerColor }), + ...(this.note && { note: this.note }), + }; + } +} + +export class Column extends SchemaElement { + name: string; + type: ColumnType; + inline_refs: InlineRef[] = []; + checks: Check[] = []; + pk?: boolean; + dbdefault?: { type: 'number' | 'string' | 'boolean' | 'expression'; value: number | string }; + increment?: boolean; + unique?: boolean; + not_null?: boolean; + note?: { value: string; token: TokenPosition }; + _hasBracketSettings?: boolean; + + constructor (token: TokenPosition, name: string, type: ColumnType) { + super(token); + this.name = name; + this.type = type; + } + + toJSON () { + const hs = this._hasBracketSettings; + return { + name: this.name, + type: this.type, + token: this.token, + inline_refs: this.inline_refs, + pk: this.pk ?? false, + ...(hs && { increment: this.increment ?? false }), + unique: this.unique ?? false, + ...(this.not_null !== undefined && { not_null: this.not_null }), + ...(this.dbdefault !== undefined && { + dbdefault: this.dbdefault.type === 'number' + ? { type: this.dbdefault.type, value: this.dbdefault.value } + : { value: this.dbdefault.value, type: this.dbdefault.type }, + }), + ...(this.note && { note: this.note }), + ...(hs && { checks: this.checks }), + }; + } +} + +export class ColumnType { + schemaName: string | null; + type_name: string; + args: string | null; + numericParams?: { precision: number; scale: number }; + lengthParam?: { length: number }; + isEnum?: boolean; + + constructor (type_name: string, schemaName: string | null = null, args: string | null = null) { + this.type_name = type_name; + this.schemaName = schemaName; + this.args = args; + } +} + +export class Index extends SchemaElement { + columns: { value: string; type: string; token: TokenPosition }[] = []; + unique?: boolean; + pk?: boolean; + name?: string; + note?: { value: string; token: TokenPosition }; + type?: string; + + toJSON () { + const hasSettings = this.pk || this.unique || this.name !== undefined || this.note || this.type !== undefined; + return { + columns: this.columns, + token: this.token, + ...(hasSettings && { pk: this.pk ?? false }), + ...(hasSettings && { unique: this.unique ?? false }), + ...(this.name !== undefined && { name: this.name }), + ...(this.note && { note: this.note }), + ...(this.type !== undefined && { type: this.type }), + }; + } +} + +export class Check extends SchemaElement { + expression: string; + name?: string; + + constructor (token: TokenPosition, expression: string) { + super(token); + this.expression = expression; + } + + toJSON () { + return { + token: this.token, + ...(this.name !== undefined && { name: this.name }), + expression: this.expression, + }; + } +} + +export class InlineRef extends SchemaElement { + schemaName: string | null; + tableName: string; + fieldNames: string[]; + relation: '>' | '<' | '-' | '<>'; + + constructor (token: TokenPosition, schemaName: string | null, tableName: string, fieldNames: string[], relation: '>' | '<' | '-' | '<>') { + super(token); + this.schemaName = schemaName; + this.tableName = tableName; + this.fieldNames = fieldNames; + this.relation = relation; + } + + toJSON () { + return { + schemaName: this.schemaName, + tableName: this.tableName, + fieldNames: this.fieldNames, + relation: this.relation, + token: this.token, + }; + } +} + +export class Note extends SchemaElement { + name: string; + content: string; + headerColor?: string; + + constructor (token: TokenPosition, name: string, content: string) { + super(token); + this.name = name; + this.content = content; + } + + toJSON () { + return { name: this.name, content: this.content, token: this.token, ...(this.headerColor && { headerColor: this.headerColor }) }; + } +} + +export class Ref extends SchemaElement { + schemaName: string | null; + name: string | null; + endpoints: RefEndpointPair; + color?: string; + onDelete?: string; + onUpdate?: string; + + constructor (token: TokenPosition, schemaName: string | null, name: string | null, endpoints: RefEndpointPair) { + super(token); + this.schemaName = schemaName; + this.name = name; + this.endpoints = endpoints; + } + + _fromInline?: boolean; + + toJSON () { + if (this._fromInline) { + return { + name: this.name, + schemaName: this.schemaName, + token: this.token, + endpoints: this.endpoints, + }; + } + return { + token: this.token, + name: this.name, + schemaName: this.schemaName, + ...(this.onDelete && { onDelete: this.onDelete }), + ...(this.onUpdate && { onUpdate: this.onUpdate }), + ...(this.color && { color: this.color }), + endpoints: this.endpoints, + }; + } +} + +export type RefEndpointPair = [RefEndpoint, RefEndpoint]; + +export class RefEndpoint extends SchemaElement { + schemaName: string | null; + tableName: string; + fieldNames: string[]; + relation: RelationCardinality; + _inlineTarget?: boolean; // First endpoint of inline ref (target table) + _inlineSource?: boolean; // Second endpoint of inline ref (source column) + + constructor (token: TokenPosition, schemaName: string | null, tableName: string, fieldNames: string[], relation: RelationCardinality) { + super(token); + this.schemaName = schemaName; + this.tableName = tableName; + this.fieldNames = fieldNames; + this.relation = relation; + } + + toJSON () { + // Inline ref target endpoint: schemaName, tableName, fieldNames, relation, token + if (this._inlineTarget) { + return { + schemaName: this.schemaName, + tableName: this.tableName, + fieldNames: this.fieldNames, + relation: this.relation, + token: this.token, + }; + } + // Inline ref source endpoint: schemaName, tableName, fieldNames, token, relation + if (this._inlineSource) { + return { + schemaName: this.schemaName, + tableName: this.tableName, + fieldNames: this.fieldNames, + token: this.token, + relation: this.relation, + }; + } + // Tuple-form (composite key) refs: tableName, schemaName, fieldNames + if (this.fieldNames.length > 1) { + return { + tableName: this.tableName, + schemaName: this.schemaName, + fieldNames: this.fieldNames, + relation: this.relation, + token: this.token, + }; + } + // Standalone ref endpoint: fieldNames, tableName, schemaName + return { + fieldNames: this.fieldNames, + tableName: this.tableName, + schemaName: this.schemaName, + relation: this.relation, + token: this.token, + }; + } +} + +export type RelationCardinality = '1' | '*'; + +export class Enum extends SchemaElement { + name: string; + schemaName: string | null; + values: EnumField[] = []; + + constructor (token: TokenPosition, name: string, schemaName: string | null) { + super(token); + this.name = name; + this.schemaName = schemaName; + } + + toJSON () { + return { values: this.values, token: this.token, name: this.name, schemaName: this.schemaName }; + } +} + +export class EnumField extends SchemaElement { + name: string; + note?: { value: string; token: TokenPosition }; + + constructor (token: TokenPosition, name: string) { + super(token); + this.name = name; + } + + toJSON () { + return { token: this.token, name: this.name, ...(this.note && { note: this.note }) }; + } +} + +export class TableGroup extends SchemaElement { + name: string | null; + schemaName: string | null; + tables: TableGroupField[] = []; + color?: string; + note?: { value: string; token: TokenPosition }; + + constructor (token: TokenPosition, name: string | null, schemaName: string | null) { + super(token); + this.name = name; + this.schemaName = schemaName; + } + + toJSON () { + return { + tables: this.tables, + token: this.token, + name: this.name, + schemaName: this.schemaName, + ...(this.color && { color: this.color }), + ...(this.note && { note: this.note }), + }; + } +} + +export class TableGroupField extends SchemaElement { + name: string; + schemaName: string | null; + + constructor (token: TokenPosition, name: string, schemaName: string | null) { + super(token); + this.name = name; + this.schemaName = schemaName; + } + + toJSON () { + return { name: this.name, schemaName: this.schemaName }; + } +} + +export class Alias extends SchemaElement { + name: string; + kind: 'table' = 'table'; + value: { tableName: string; schemaName: string | null }; + + constructor (token: TokenPosition, name: string, value: { tableName: string; schemaName: string | null }) { + super(token); + this.name = name; + this.value = value; + } + + toJSON () { + return { + name: this.name, + kind: this.kind, + value: this.value, + }; + } +} + +export class TablePartial extends SchemaElement { + name: string; + fields: Column[] = []; + indexes: Index[] = []; + checks: Check[] = []; + headerColor?: string; + note?: { value: string; token: TokenPosition }; + + constructor (token: TokenPosition, name: string) { + super(token); + this.name = name; + } + + toJSON () { + return { + name: this.name, + fields: this.fields, + token: this.token, + indexes: this.indexes, + checks: this.checks, + ...(this.headerColor && { headerColor: this.headerColor }), + ...(this.note && { note: this.note }), + }; + } +} + +export class TablePartialInjection extends SchemaElement { + name: string; + order: number; + + constructor (token: TokenPosition, name: string, order: number) { + super(token); + this.name = name; + this.order = order; + } + + toJSON () { + return { order: this.order, token: this.token, name: this.name }; + } +} + +export type RecordValueType = 'string' | 'bool' | 'integer' | 'real' | 'date' | 'time' | 'datetime' | string; + +export class RecordValue { + value: any; + type: RecordValueType; + + constructor (value: any, type: RecordValueType) { + this.value = value; + this.type = type; + } +} + +export class TableRecord extends SchemaElement { + schemaName: string | undefined; + tableName: string; + columns: string[] = []; + values: RecordValue[][] = []; + + constructor (token: TokenPosition, tableName: string, schemaName?: string) { + super(token); + this.tableName = tableName; + this.schemaName = schemaName; + } +} + +export class Project extends SchemaElement { + name: string | null; + note?: { value: string; token: TokenPosition }; + [key: string]: any; // Allow dynamic properties like database_type + + constructor (token: TokenPosition, name: string | null) { + super(token); + this.name = name; + } + + toJSON () { + // Collect extra props (database_type, etc.) - anything that's not a known field + const known = new Set(['token', 'name', 'note', 'enums', 'refs', 'tableGroups', 'tables', 'tablePartials']); + const extra: Record = {}; + for (const key of Object.keys(this)) { + if (!known.has(key)) { + extra[key] = (this as any)[key]; + } + } + return { + enums: [], + refs: [], + tableGroups: [], + tables: [], + tablePartials: [], + token: this.token, + name: this.name, + ...(this.note && { note: this.note }), + ...extra, + }; + } +} diff --git a/packages/dbml-parse/src/core/types/symbols.ts b/packages/dbml-parse/src/core/types/symbols.ts new file mode 100644 index 000000000..8275605d5 --- /dev/null +++ b/packages/dbml-parse/src/core/types/symbols.ts @@ -0,0 +1,94 @@ +import { SyntaxNode } from '@/core/types/nodes'; +import type { Internable } from '@/core/types/internable'; + +export const enum SymbolKind { + Schema = 'Schema', + + Table = 'Table', + Column = 'Column', + + TableGroup = 'TableGroup', + TableGroupField = 'TableGroup field', + + Enum = 'Enum', + EnumField = 'Enum field', + + Note = 'Note', + + TablePartial = 'TablePartial', + TablePartialField = 'TablePartial field', + PartialInjection = 'PartialInjection', + + Project = 'Project', + ProjectField = 'Project field', + + Records = 'Records', + RecordsField = 'Records field', + + Indexes = 'Indexes', + IndexesField = 'Indexes field', + + Checks = 'Checks', + + Ref = 'Ref', + + Program = 'Program', +} + +declare const __nodeSymbolBrand: unique symbol; +export type NodeSymbolId = number & { readonly [__nodeSymbolBrand]: true }; + +declare const __internedNodeSymbolBrand: unique symbol; +export type InternedNodeSymbol = string & { readonly [__internedNodeSymbolBrand]: true }; + +export class NodeSymbolIdGenerator { + private id = 0; + + reset () { + this.id = 0; + } + + nextId (): NodeSymbolId { + return this.id++ as NodeSymbolId; + } +} + +export class NodeSymbol implements Internable { + /* @internal only accessed in tests */ + static _idGenerator = new NodeSymbolIdGenerator(); + + id: NodeSymbolId; + kind: SymbolKind; + declaration?: SyntaxNode; + references: SyntaxNode[] = []; + + constructor ({ + kind, + declaration, + }: { + kind: SymbolKind; + declaration?: SyntaxNode; + }) { + this.id = NodeSymbol._idGenerator.nextId(); + + this.kind = kind; + this.declaration = declaration; + } + + intern (): InternedNodeSymbol { + return `symbol@${this.id}` as InternedNodeSymbol; + } + + isKind (...kinds: SymbolKind[]): boolean { + return kinds.includes(this.kind); + } +} + +export class SchemaSymbol extends NodeSymbol { + name: string; + + constructor (name: string) { + super({ kind: SymbolKind.Schema }); + this.name = name; + } +} diff --git a/packages/dbml-parse/src/core/lexer/tokens.ts b/packages/dbml-parse/src/core/types/tokens.ts similarity index 98% rename from packages/dbml-parse/src/core/lexer/tokens.ts rename to packages/dbml-parse/src/core/types/tokens.ts index 64ba3d4cf..b90decfc1 100644 --- a/packages/dbml-parse/src/core/lexer/tokens.ts +++ b/packages/dbml-parse/src/core/types/tokens.ts @@ -1,4 +1,4 @@ -import { Position } from '@/core/types'; +import { Position } from '@/core/types/position'; export enum SyntaxTokenKind { SPACE = '', diff --git a/packages/dbml-parse/src/core/utils.ts b/packages/dbml-parse/src/core/utils.ts deleted file mode 100644 index b81589591..000000000 --- a/packages/dbml-parse/src/core/utils.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { SyntaxToken } from './lexer/tokens'; -import { - LiteralNode, PrefixExpressionNode, PrimaryExpressionNode, SyntaxNode, -} from './parser/nodes'; -import { getTokenFullEnd, getTokenFullStart } from './lexer/utils'; - -export function isAlphaOrUnderscore (char: string): boolean { - // Match any letters, accents (some characters are denormalized so the accent and the main character are two separate characters) and underscore - // \p{L} is used to match letters - // \p{M} is used to match accents - // References: - // https://unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt - // https://www.compart.com/en/unicode/category/Mn - // https://www.compart.com/en/unicode/category/Me - // https://www.compart.com/en/unicode/category/Mc - return !!char.match(/(\p{L}|_|\p{M})/gu); -} - -export function isDigit (char: string): boolean { - if (!char) return false; - const c = char[0]; - - return c >= '0' && c <= '9'; -} - -// Check if a character is a valid hexadecimal character -export function isHexChar (char: string): boolean { - const [c] = char; - - return isDigit(c) || (isAlphaOrUnderscore(c) && c.toLowerCase() >= 'a' && c.toLowerCase() <= 'f'); -} - -export function isAlphaNumeric (char: string): boolean { - return isAlphaOrUnderscore(char) || isDigit(char); -} - -export function alternateLists (firstList: T[], secondList: S[]): (T | S)[] { - const res: (T | S)[] = []; - const minLength = Math.min(firstList.length, secondList.length); - for (let i = 0; i < minLength; i += 1) { - res.push(firstList[i], secondList[i]); - } - res.push(...firstList.slice(minLength), ...secondList.slice(minLength)); - - return res; -} - -export function isOffsetWithinFullSpan ( - offset: number, - nodeOrToken: SyntaxNode | SyntaxToken, -): boolean { - if (nodeOrToken instanceof SyntaxToken) { - return offset >= getTokenFullStart(nodeOrToken) && offset < getTokenFullEnd(nodeOrToken); - } - - return offset >= nodeOrToken.fullStart && offset < nodeOrToken.fullEnd; -} - -export function isOffsetWithinSpan (offset: number, nodeOrToken: SyntaxNode | SyntaxToken): boolean { - return offset >= nodeOrToken.start && offset < nodeOrToken.end; -} - -export function returnIfIsOffsetWithinFullSpan ( - offset: number, - node?: SyntaxNode, -): SyntaxNode | undefined; -export function returnIfIsOffsetWithinFullSpan ( - offset: number, - token?: SyntaxToken, -): SyntaxToken | undefined; -export function returnIfIsOffsetWithinFullSpan ( - offset: number, - nodeOrToken?: SyntaxNode | SyntaxToken, -): SyntaxNode | SyntaxToken | undefined { - if (!nodeOrToken) { - return undefined; - } - - return isOffsetWithinFullSpan(offset, nodeOrToken) ? nodeOrToken : undefined; -} - -export function getNumberTextFromExpression (node: PrimaryExpressionNode | PrefixExpressionNode): string { - if (node instanceof PrefixExpressionNode) { - return `${node.op?.value}${getNumberTextFromExpression(node.expression!)}`; - } - return (node.expression as LiteralNode).literal!.value; -} - -export function parseNumber (node: PrefixExpressionNode | PrimaryExpressionNode): number { - if (node instanceof PrefixExpressionNode) { - const op = node.op?.value; - if (op === '-') return -parseNumber(node.expression!); - return parseNumber(node.expression!); - } - return Number.parseFloat((node.expression as LiteralNode).literal!.value); -} diff --git a/packages/dbml-parse/src/core/utils/chars.ts b/packages/dbml-parse/src/core/utils/chars.ts new file mode 100644 index 000000000..c49f85880 --- /dev/null +++ b/packages/dbml-parse/src/core/utils/chars.ts @@ -0,0 +1,31 @@ +export function isAlphaOrUnderscore (char: string): boolean { + return !!char.match(/(\p{L}|_|\p{M})/gu); +} + +export function isDigit (char: string): boolean { + if (!char) return false; + const c = char[0]; + + return c >= '0' && c <= '9'; +} + +export function isHexChar (char: string): boolean { + const [c] = char; + + return isDigit(c) || (isAlphaOrUnderscore(c) && c.toLowerCase() >= 'a' && c.toLowerCase() <= 'f'); +} + +export function isAlphaNumeric (char: string): boolean { + return isAlphaOrUnderscore(char) || isDigit(char); +} + +export function alternateLists (firstList: T[], secondList: S[]): (T | S)[] { + const res: (T | S)[] = []; + const minLength = Math.min(firstList.length, secondList.length); + for (let i = 0; i < minLength; i += 1) { + res.push(firstList[i], secondList[i]); + } + res.push(...firstList.slice(minLength), ...secondList.slice(minLength)); + + return res; +} diff --git a/packages/dbml-parse/src/core/utils/expression.ts b/packages/dbml-parse/src/core/utils/expression.ts new file mode 100644 index 000000000..f8b80c06b --- /dev/null +++ b/packages/dbml-parse/src/core/utils/expression.ts @@ -0,0 +1,224 @@ +import { + IdentiferStreamNode, + InfixExpressionNode, + LiteralNode, + PrefixExpressionNode, + PrimaryExpressionNode, + SyntaxNode, + TupleExpressionNode, + VariableNode, + ElementDeclarationNode, + FunctionApplicationNode, + BlockExpressionNode, + ProgramNode, + ListExpressionNode, + AttributeNode, +} from '@/core/types/nodes'; +import type { ElementKind, SettingName } from '@/core/types/keywords'; +import { SyntaxToken, SyntaxTokenKind } from '@/core/types/tokens'; +import { destructureComplexVariable, extractVariableFromExpression, extractVarNameFromPrimaryVariable } from '@/core/syntax/utils'; + +const NUMERIC_LITERAL_PREFIX = ['+', '-']; + +export function getNumberTextFromExpression (node: PrimaryExpressionNode | PrefixExpressionNode): string { + if (node instanceof PrefixExpressionNode) { + return `${node.op?.value}${getNumberTextFromExpression(node.expression!)}`; + } + return (node.expression as LiteralNode).literal!.value; +} + +export function parseNumber (node: PrefixExpressionNode | PrimaryExpressionNode): number { + if (node instanceof PrefixExpressionNode) { + const op = node.op?.value; + if (op === '-') return -parseNumber(node.expression!); + return parseNumber(node.expression!); + } + return Number.parseFloat((node.expression as LiteralNode).literal!.value); +} + +export type SignedNumberExpression = + (PrimaryExpressionNode & { expression: LiteralNode & { literal: { kind: SyntaxTokenKind.NUMERIC_LITERAL } } }) + | (PrefixExpressionNode & { op: '-' | '+'; expression: SignedNumberExpression }); + +export function isExpressionASignedNumberExpression (value?: SyntaxNode): value is SignedNumberExpression { + if (value instanceof PrefixExpressionNode) { + if (!NUMERIC_LITERAL_PREFIX.includes(value.op!.value)) return false; + return isExpressionASignedNumberExpression(value.expression); + } + return ( + value instanceof PrimaryExpressionNode + && value.expression instanceof LiteralNode + && value.expression.literal?.kind === SyntaxTokenKind.NUMERIC_LITERAL + ); +} + +export function extractVariableNode (value?: unknown): SyntaxToken | undefined { + if (isExpressionAVariableNode(value)) { + return value.expression.variable; + } + return undefined; +} + +export function isExpressionAQuotedString (value?: unknown): value is PrimaryExpressionNode + & ( + | { expression: VariableNode & { variable: SyntaxToken & { kind: SyntaxTokenKind.QUOTED_STRING } } } + | { + expression: LiteralNode & { + literal: SyntaxToken & { kind: SyntaxTokenKind.STRING_LITERAL }; + }; + } + ) { + return ( + value instanceof PrimaryExpressionNode + && ( + ( + value.expression instanceof VariableNode + && value.expression.variable instanceof SyntaxToken + && value.expression.variable.kind === SyntaxTokenKind.QUOTED_STRING + ) + || ( + value.expression instanceof LiteralNode + && value.expression.literal?.kind === SyntaxTokenKind.STRING_LITERAL + ) + ) + ); +} + +export function isExpressionAVariableNode ( + value?: unknown, +): value is PrimaryExpressionNode & { expression: VariableNode & { variable: SyntaxToken } } { + return ( + value instanceof PrimaryExpressionNode + && value.expression instanceof VariableNode + && value.expression.variable instanceof SyntaxToken + ); +} + +export function isExpressionAnIdentifierNode (value?: unknown): value is PrimaryExpressionNode & { + expression: VariableNode & { variable: { kind: SyntaxTokenKind.IDENTIFIER } }; +} { + return ( + value instanceof PrimaryExpressionNode + && value.expression instanceof VariableNode + && value.expression.variable?.kind === SyntaxTokenKind.IDENTIFIER + ); +} + +type AccessExpression = InfixExpressionNode & { + leftExpression: SyntaxNode; + rightExpression: SyntaxNode; + op: SyntaxToken & { value: '.' }; +}; + +type DotDelimitedIdentifier = PrimaryExpressionNode | (AccessExpression & { + rightExpression: AccessExpression | PrimaryExpressionNode; +}); + +export function isAccessExpression (node?: SyntaxNode): node is AccessExpression { + return ( + node instanceof InfixExpressionNode + && node.leftExpression instanceof SyntaxNode + && node.rightExpression instanceof SyntaxNode + && node.op?.value === '.' + ); +} + +export function isDotDelimitedIdentifier (node?: SyntaxNode): node is DotDelimitedIdentifier { + if (isExpressionAVariableNode(node)) return true; + return isAccessExpression(node) && isExpressionAVariableNode(node.rightExpression) && isDotDelimitedIdentifier(node.leftExpression); +} + +export function extractStringFromIdentifierStream (stream?: IdentiferStreamNode): string | undefined { + if (stream === undefined) return undefined; + const name = stream.identifiers.map((identifier) => identifier.value).join(' '); + if (name === '') return undefined; + return name; +} + +export function getElementNameString (element?: ElementDeclarationNode): string | undefined { + const ss = destructureComplexVariable(element?.name); + return ss !== undefined ? ss.join('.') : undefined; +} + +export function isAsKeyword ( + token?: SyntaxToken, +): token is SyntaxToken & { kind: SyntaxTokenKind.IDENTIFIER; value: 'as' } { + return token?.kind === SyntaxTokenKind.IDENTIFIER && token.value.toLowerCase() === 'as'; +} + +export function isTupleOfVariables (value?: SyntaxNode): value is TupleExpressionNode & { + elementList: (PrimaryExpressionNode & { expression: VariableNode })[]; +} { + return value instanceof TupleExpressionNode && value.elementList.every(isExpressionAVariableNode); +} + +export function isRelationshipOp (op?: string): op is '>' | '<' | '-' | '<>' { + return op === '-' || op === '<>' || op === '>' || op === '<'; +} + +export function getBody (node?: ElementDeclarationNode): (FunctionApplicationNode | ElementDeclarationNode)[] { + if (!node?.body) return []; + return node.body instanceof BlockExpressionNode ? node.body.body : [node.body]; +} + +// Return whether `node` is an ElementDeclarationNode of kind `kind` +export function isElementNode (node: SyntaxNode, kind: ElementKind): node is ElementDeclarationNode { + return node instanceof ElementDeclarationNode && node.isKind(kind); +} + +// Return whether `node` is a ProgramNode +export function isProgramNode (node: SyntaxNode): node is ProgramNode { + return node instanceof ProgramNode; +} + +// Return whether `node` is a field of some element +export function isElementFieldNode (node: SyntaxNode, kind: ElementKind): node is FunctionApplicationNode { + return node instanceof FunctionApplicationNode + && node.parentElement instanceof ElementDeclarationNode + && node.parentElement.isKind(kind); +} + +// Return whether `node` is within some element of a given kind +export function isInsideElementBody (node: SyntaxNode, kind: ElementKind): boolean { + const parentElement = node.parentElement; + return parentElement instanceof ElementDeclarationNode + && parentElement.isKind(kind) + && !!parentElement.body + && parentElement.body.strictlyContains(node); +} + +// Return whether `node` is within the n-th arg of a field +// `callee` -> 0th arg +// `args[0]` -> 1th arg +// `args[1]` -> 2nd arg +// ... +export function isWithinNthArgOfField (node: SyntaxNode, nth: number): boolean { + const parentField = node.parentOfKind(FunctionApplicationNode); + if (!parentField) { + return false; + } + if (nth < 0) return false; + if (nth === 0) { + if (!parentField.callee) return false; + return parentField.callee.containsEq(node); + } + const arg = parentField.args[nth - 1]; + if (!arg) return false; + return arg.containsEq(node); +} + +// Return whether `node` is within a setting list +export function isInsideSettingList (node: SyntaxNode): boolean { + const parentField = node.parentOfKind(ListExpressionNode); + return !!parentField; +} + +export function isInsideSettingValue (node: SyntaxNode, settingName: SettingName): boolean { + const attributeNode = node.parentOfKind(AttributeNode); + if (!attributeNode) return false; + const name = attributeNode.name instanceof PrimaryExpressionNode ? extractVariableFromExpression(attributeNode.name) : extractStringFromIdentifierStream(attributeNode.name); + if (name?.toLowerCase() !== settingName) { + return false; + } + return !!attributeNode.value?.containsEq(node); +} diff --git a/packages/dbml-parse/src/core/utils/span.ts b/packages/dbml-parse/src/core/utils/span.ts new file mode 100644 index 000000000..3b84ad080 --- /dev/null +++ b/packages/dbml-parse/src/core/utils/span.ts @@ -0,0 +1,37 @@ +import { SyntaxNode } from '@/core/types/nodes'; +import { SyntaxToken } from '@/core/types/tokens'; +import { getTokenFullEnd, getTokenFullStart } from '@/core/syntax/lexer/utils'; + +export function isOffsetWithinFullSpan ( + offset: number, + nodeOrToken: SyntaxNode | SyntaxToken, +): boolean { + if (nodeOrToken instanceof SyntaxToken) { + return offset >= getTokenFullStart(nodeOrToken) && offset < getTokenFullEnd(nodeOrToken); + } + + return offset >= nodeOrToken.fullStart && offset < nodeOrToken.fullEnd; +} + +export function isOffsetWithinSpan (offset: number, nodeOrToken: SyntaxNode | SyntaxToken): boolean { + return offset >= nodeOrToken.start && offset < nodeOrToken.end; +} + +export function returnIfIsOffsetWithinFullSpan ( + offset: number, + node?: SyntaxNode, +): SyntaxNode | undefined; +export function returnIfIsOffsetWithinFullSpan ( + offset: number, + token?: SyntaxToken, +): SyntaxToken | undefined; +export function returnIfIsOffsetWithinFullSpan ( + offset: number, + nodeOrToken?: SyntaxNode | SyntaxToken, +): SyntaxNode | SyntaxToken | undefined { + if (!nodeOrToken) { + return undefined; + } + + return isOffsetWithinFullSpan(offset, nodeOrToken) ? nodeOrToken : undefined; +} diff --git a/packages/dbml-parse/src/core/utils/validate.ts b/packages/dbml-parse/src/core/utils/validate.ts new file mode 100644 index 000000000..40b64ab1e --- /dev/null +++ b/packages/dbml-parse/src/core/utils/validate.ts @@ -0,0 +1,197 @@ +import { + ArrayNode, + AttributeNode, + BlockExpressionNode, + CallExpressionNode, + ElementDeclarationNode, + FunctionExpressionNode, + ListExpressionNode, + LiteralNode, + PrefixExpressionNode, + PrimaryExpressionNode, + SyntaxNode, + VariableNode, +} from '@/core/types/nodes'; +import { SyntaxToken, SyntaxTokenKind } from '@/core/types/tokens'; +import { CompileError, CompileErrorCode } from '@/core/types/errors'; +import Report from '@/core/types/report'; +import { + destructureComplexVariable, + destructureMemberAccessExpression, +} from '@/core/syntax/utils'; +import { isHexChar } from './chars'; +import { + isExpressionAQuotedString, + isExpressionASignedNumberExpression, + isExpressionAVariableNode, + isExpressionAnIdentifierNode, + isAccessExpression, + isDotDelimitedIdentifier, + isRelationshipOp, + extractStringFromIdentifierStream, +} from './expression'; +import type { SettingName } from '../types/keywords'; +import { KEYWORDS_OF_DEFAULT_SETTING, NUMERIC_LITERAL_PREFIX } from '@/constants'; + +export function isValidName (nameNode: SyntaxNode): boolean { + return !!destructureComplexVariable(nameNode); +} + +export function isValidAlias ( + aliasNode: SyntaxNode, +): aliasNode is PrimaryExpressionNode & { expression: VariableNode } { + return isSimpleName(aliasNode); +} + +export function isSimpleName ( + nameNode: SyntaxNode, +): nameNode is PrimaryExpressionNode & { expression: VariableNode } { + return nameNode instanceof PrimaryExpressionNode && nameNode.expression instanceof VariableNode; +} + +export function isValidSettingList ( + settingListNode: SyntaxNode, +): settingListNode is ListExpressionNode { + return settingListNode instanceof ListExpressionNode; +} + +export function hasComplexBody ( + node: ElementDeclarationNode, +): node is ElementDeclarationNode & { body: BlockExpressionNode; bodyColon: undefined } { + return node.body instanceof BlockExpressionNode && !node.bodyColon; +} + +export function hasSimpleBody ( + node: ElementDeclarationNode, +): node is ElementDeclarationNode & { bodyColon: SyntaxToken } { + return !!node.bodyColon; +} + +export function isValidPartialInjection ( + node?: SyntaxNode, +): node is PrefixExpressionNode & { op: { value: '~' } } { + return node instanceof PrefixExpressionNode && node.op?.value === '~' && isExpressionAVariableNode(node.expression); +} + +export function isValidColor (value?: SyntaxNode): boolean { + if ( + !(value instanceof PrimaryExpressionNode) + || !(value.expression instanceof LiteralNode) + || !(value.expression.literal?.kind === SyntaxTokenKind.COLOR_LITERAL) + ) { + return false; + } + + const color = value.expression.literal.value; + + if (color.length !== 4 && color.length !== 7) return false; + if (color[0] !== '#') return false; + + for (let i = 1; i < color.length; i += 1) { + if (!isHexChar(color[i])) return false; + } + + return true; +} + +export function isVoid (value?: SyntaxNode): boolean { + return value === undefined; +} + +export function isValidDefaultValue (value?: SyntaxNode): boolean { + if (isExpressionAQuotedString(value)) return true; + if (isExpressionASignedNumberExpression(value)) return true; + + if (isExpressionAnIdentifierNode(value) && KEYWORDS_OF_DEFAULT_SETTING.includes(value.expression.variable.value.toLowerCase())) { + return true; + } + + if ( + value instanceof PrefixExpressionNode + && NUMERIC_LITERAL_PREFIX.includes(value.op?.value as any) + && isExpressionASignedNumberExpression(value.expression) + ) { + return true; + } + + if (value instanceof FunctionExpressionNode) return true; + + if (!value) return false; + + if (!isDotDelimitedIdentifier(value)) return false; + + const fragments = destructureMemberAccessExpression(value); + return fragments?.length === 2 || fragments?.length === 3; +} + +export function isUnaryRelationship (value?: SyntaxNode): value is PrefixExpressionNode { + if (!(value instanceof PrefixExpressionNode)) return false; + if (!isRelationshipOp(value.op?.value)) return false; + + const variables = destructureComplexVariable(value.expression); + return variables !== undefined && variables.length > 0; +} + +export function isValidColumnType (type: SyntaxNode): boolean { + if ( + !( + type instanceof CallExpressionNode + || isAccessExpression(type) + || type instanceof PrimaryExpressionNode + || type instanceof ArrayNode + ) + ) { + return false; + } + + while (type instanceof CallExpressionNode || type instanceof ArrayNode) { + if (type instanceof CallExpressionNode) { + if (type.callee === undefined || type.argumentList === undefined) return false; + if (!type.argumentList.elementList.every((e) => isExpressionASignedNumberExpression(e) || isExpressionAQuotedString(e) || isExpressionAnIdentifierNode(e))) { + return false; + } + type = type.callee; + } else if (type instanceof ArrayNode) { + if (type.array === undefined || type.indexer === undefined) return false; + if (!type.indexer.elementList.every((attribute) => !attribute.colon && !attribute.value && isExpressionASignedNumberExpression(attribute.name))) { + return false; + } + type = type.array; + } + } + + const variables = destructureComplexVariable(type); + return variables !== undefined && variables.length > 0; +} + +export type Settings = Map; + +export function collectSettings (settingList?: ListExpressionNode): Report { + const map: Settings = new Map(); + const errors: CompileError[] = []; + + if (!settingList) { + return new Report(map); + } + + settingList.elementList.forEach((attribute) => { + if (!attribute.name) return; + + if (attribute.name instanceof PrimaryExpressionNode) { + errors.push(new CompileError(CompileErrorCode.INVALID_SETTINGS, 'A setting name must be a stream of identifiers', attribute.name)); + return; + } + + const name = extractStringFromIdentifierStream(attribute.name)?.toLowerCase(); + if (!name) return; + + const existing = map.get(name); + if (existing) { + existing.push(attribute); + } else { + map.set(name, [attribute]); + } + }); + + return new Report(map, errors); +} diff --git a/packages/dbml-parse/src/index.ts b/packages/dbml-parse/src/index.ts index 4601e4f88..9a7088317 100644 --- a/packages/dbml-parse/src/index.ts +++ b/packages/dbml-parse/src/index.ts @@ -1,12 +1,11 @@ import Compiler from '@/compiler/index'; -import * as services from '@/services/index'; +// TODO: migrate services +// import * as services from '@/services/index'; // Export the types that playground and other consumers need export { ElementKind, -} from '@/core/analyzer/types'; - -export * from '@/core/interpreter/records/utils'; +} from '@/core/types/keywords'; export { // Core AST node types @@ -14,25 +13,19 @@ export { ElementDeclarationNode, ProgramNode, SyntaxNodeKind, - type SyntaxNodeId, -} from '@/core/parser/nodes'; +} from '@/core/types/nodes'; export { // Token types SyntaxToken, SyntaxTokenKind, -} from '@/core/lexer/tokens'; +} from '@/core/types/tokens'; export { // Error types CompileError, CompileErrorCode, -} from '@/core/errors'; - -export { - // Position interface - type Position, -} from '@/core/types'; +} from '@/core/types/errors'; export { // Scope kinds from compiler @@ -46,16 +39,4 @@ export { addDoubleQuoteIfNeeded, } from '@/compiler/index'; -// Export interpreted types for structured data -export { - type Database, - type Table, - type Column, - type Enum, - type Ref, - type Project, - type TableGroup, - type TablePartial, -} from '@/core/interpreter/types'; - -export { Compiler, services }; +export { Compiler }; diff --git a/packages/dbml-parse/src/services/definition/provider.ts b/packages/dbml-parse/src/services/definition/provider.ts index 6677aee20..9121305ec 100644 --- a/packages/dbml-parse/src/services/definition/provider.ts +++ b/packages/dbml-parse/src/services/definition/provider.ts @@ -3,7 +3,8 @@ import { } from '@/services/types'; import { getOffsetFromMonacoPosition } from '@/services/utils'; import Compiler from '@/compiler'; -import { SyntaxNode, SyntaxNodeKind } from '@/core/parser/nodes'; +import { SyntaxNode, SyntaxNodeKind } from '@/core/types/nodes'; +import { UNHANDLED } from '@/constants'; export default class DBMLDefinitionProvider implements DefinitionProvider { private compiler: Compiler; @@ -18,18 +19,22 @@ export default class DBMLDefinitionProvider implements DefinitionProvider { const containers = [...this.compiler.container.stack(offset)]; while (containers.length !== 0) { const node = containers.pop(); + if (!node) continue; - if (!node?.referee) continue; + const refereeResult = this.compiler.nodeReferee(node); + if (refereeResult.hasValue(UNHANDLED)) continue; + const referee = refereeResult.getValue(); + if (!referee) continue; let declaration: SyntaxNode | undefined; if ( - node.referee?.declaration + referee.declaration && [ SyntaxNodeKind.PRIMARY_EXPRESSION, SyntaxNodeKind.VARIABLE, - ].includes(node?.kind) + ].includes(node.kind) ) { - ({ declaration } = node.referee); + ({ declaration } = referee); } if (declaration) { diff --git a/packages/dbml-parse/src/services/diagnostics/provider.ts b/packages/dbml-parse/src/services/diagnostics/provider.ts index b72471cf8..99398af42 100644 --- a/packages/dbml-parse/src/services/diagnostics/provider.ts +++ b/packages/dbml-parse/src/services/diagnostics/provider.ts @@ -1,8 +1,8 @@ import type Compiler from '@/compiler'; -import type { CompileError, CompileWarning } from '@/core/errors'; +import type { CompileError, CompileWarning } from '@/core/types/errors'; import { MarkerSeverity, MarkerData } from '@/services/types'; -import type { SyntaxNode } from '@/core/parser/nodes'; -import type { SyntaxToken } from '@/core/lexer/tokens'; +import type { SyntaxNode } from '@/core/types/nodes'; +import type { SyntaxToken } from '@/core/types/tokens'; // This is the same format that dbdiagram-frontend uses interface Diagnostic { diff --git a/packages/dbml-parse/src/services/references/provider.ts b/packages/dbml-parse/src/services/references/provider.ts index 340381084..3013b1c07 100644 --- a/packages/dbml-parse/src/services/references/provider.ts +++ b/packages/dbml-parse/src/services/references/provider.ts @@ -1,9 +1,10 @@ -import { getOffsetFromMonacoPosition } from '@/services/utils'; +import { getOffsetFromMonacoPosition, getNodeSymbol } from '@/services/utils'; import Compiler from '@/compiler'; -import { SyntaxNodeKind } from '@/core/parser/nodes'; +import { SyntaxNodeKind } from '@/core/types/nodes'; import { Location, ReferenceProvider, TextModel, Position, } from '@/services/types'; +import { UNHANDLED } from '@/constants'; export default class DBMLReferencesProvider implements ReferenceProvider { private compiler: Compiler; @@ -27,17 +28,21 @@ export default class DBMLReferencesProvider implements ReferenceProvider { SyntaxNodeKind.PRIMARY_EXPRESSION, ].includes(node?.kind) ) { - const { symbol } = node; - if (symbol?.references.length) { - return symbol.references.map(({ startPos, endPos }) => ({ - range: { - startColumn: startPos.column + 1, - startLineNumber: startPos.line + 1, - endColumn: endPos.column + 1, - endLineNumber: endPos.line + 1, - }, - uri, - })); + const symbol = getNodeSymbol(this.compiler, node); + const referencesResult = symbol ? this.compiler.symbolReferences(symbol) : undefined; + if (referencesResult && !referencesResult.hasValue(UNHANDLED)) { + const references = referencesResult.getValue(); + if (references && references.length > 0) { + return references.map(({ startPos, endPos }) => ({ + range: { + startColumn: startPos.column + 1, + startLineNumber: startPos.line + 1, + endColumn: endPos.column + 1, + endLineNumber: endPos.line + 1, + }, + uri, + })); + } } } } diff --git a/packages/dbml-parse/src/services/suggestions/provider.ts b/packages/dbml-parse/src/services/suggestions/provider.ts index d59b5eede..297491635 100644 --- a/packages/dbml-parse/src/services/suggestions/provider.ts +++ b/packages/dbml-parse/src/services/suggestions/provider.ts @@ -1,15 +1,14 @@ import { destructureMemberAccessExpression, extractVariableFromExpression, - getElementKind, -} from '@/core/analyzer/utils'; +} from '@/core/syntax/utils'; import { extractStringFromIdentifierStream, isExpressionAVariableNode, -} from '@/core/parser/utils'; +} from '@/core/utils/expression'; import Compiler, { ScopeKind } from '@/compiler'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; -import { isOffsetWithinSpan } from '@/core/utils'; +import { SyntaxToken, SyntaxTokenKind } from '@/core/types/tokens'; +import { isOffsetWithinSpan } from '@/core/utils/span'; import { type CompletionList, type TextModel, @@ -18,8 +17,8 @@ import { CompletionItemKind, CompletionItemInsertTextRule, } from '@/services/types'; -import { TableSymbol, type NodeSymbol } from '@/core/analyzer/symbol/symbols'; -import { SymbolKind, destructureIndex } from '@/core/analyzer/symbol/symbolIndex'; +import { type NodeSymbol } from '@/core/types/symbols'; +import { SymbolKind } from '@/core/types/symbols'; import { pickCompletionItemKind, shouldPrependSpace, @@ -44,10 +43,11 @@ import { ProgramNode, SyntaxNode, TupleExpressionNode, -} from '@/core/parser/nodes'; -import { getOffsetFromMonacoPosition } from '@/services/utils'; -import { isComment } from '@/core/lexer/utils'; -import { ElementKind, SettingName } from '@/core/analyzer/types'; +} from '@/core/types/nodes'; +import { getOffsetFromMonacoPosition, getNodeSymbol, getSymbolMembers } from '@/services/utils'; +import { isComment } from '@/core/syntax/lexer/utils'; +import { ElementKind, SettingName } from '@/core/types/keywords'; +import { UNHANDLED } from '@/constants'; export default class DBMLCompletionItemProvider implements CompletionItemProvider { private compiler: Compiler; @@ -211,18 +211,23 @@ function suggestMembersOfSymbol ( symbol: NodeSymbol, acceptedKinds: SymbolKind[], ): CompletionList { + const members = getSymbolMembers(compiler, symbol); return addQuoteToSuggestionIfNeeded({ - suggestions: compiler.symbol - .members(symbol) - .filter(({ kind }) => acceptedKinds.includes(kind)) - .map(({ name, kind }) => ({ - label: name, - insertText: name, - insertTextRules: CompletionItemInsertTextRule.KeepWhitespace, - kind: pickCompletionItemKind(kind), - sortText: pickCompletionItemKind(kind).toString().padStart(2, '0'), - range: undefined as any, - })), + suggestions: members + .filter((member) => acceptedKinds.includes(member.kind)) + .map((member) => { + const nameResult = member.declaration ? compiler.fullname(member.declaration) : undefined; + const name = (nameResult && !nameResult.hasValue(UNHANDLED)) ? nameResult.getValue()?.at(-1) ?? '' : ''; + return { + label: name, + insertText: name, + insertTextRules: CompletionItemInsertTextRule.KeepWhitespace, + kind: pickCompletionItemKind(member.kind), + sortText: pickCompletionItemKind(member.kind).toString().padStart(2, '0'), + range: undefined as any, + }; + }) + .filter((s) => s.label !== ''), }); } @@ -239,8 +244,8 @@ function suggestNamesInScope ( let curElement: SyntaxNode | undefined = parent; const res: CompletionList = { suggestions: [] }; while (curElement) { - if (curElement?.symbol?.symbolTable) { - const { symbol } = curElement; + const symbol = getNodeSymbol(compiler, curElement); + if (symbol) { res.suggestions.push( ...suggestMembersOfSymbol(compiler, symbol, acceptedKinds).suggestions, ); @@ -266,11 +271,14 @@ function suggestInTuple (compiler: Compiler, offset: number, tupleContainer: Tup // Check if we're in a Records element header if ( element instanceof ElementDeclarationNode - && getElementKind(element).unwrap_or(undefined) === ElementKind.Records + && element.isKind(ElementKind.Records) && !(element.name instanceof CallExpressionNode) && isOffsetWithinElementHeader(offset, element) ) { - const tableSymbol = element.parent?.symbol || element.name?.referee; + const parentSymbol = element.parent ? getNodeSymbol(compiler, element.parent) : undefined; + const refereeResult = element.name ? compiler.nodeReferee(element.name) : undefined; + const refereeSymbol = (refereeResult && !refereeResult.hasValue(UNHANDLED)) ? refereeResult.getValue() : undefined; + const tableSymbol = parentSymbol || refereeSymbol; if (tableSymbol) { const suggestions = suggestMembersOfSymbol(compiler, tableSymbol, [SymbolKind.Column]); // If the user already typed some columns, we do not suggest "all columns" anymore @@ -342,7 +350,7 @@ function suggestInAttribute ( const res = suggestAttributeValue( compiler, offset, - extractStringFromIdentifierStream(container.name).unwrap_or(''), + extractStringFromIdentifierStream(container.name) ?? '', ); return (token?.kind === SyntaxTokenKind.COLON && shouldPrependSpace(token, offset)) ? prependSpace(res) : res; @@ -517,29 +525,70 @@ function suggestAttributeValue ( return noSuggestions(); } +// Resolve a name stack (e.g. ['schema', 'table']) to matching symbols +// by walking from the scope element's symbol through its members +function resolveNameStack ( + compiler: Compiler, + nameStack: string[], + scopeElement: SyntaxNode | undefined, +): NodeSymbol[] { + if (!scopeElement) return []; + + // Collect all symbols from the scope hierarchy + let candidates: NodeSymbol[] = []; + let curElement: SyntaxNode | undefined = scopeElement; + while (curElement) { + const symbol = getNodeSymbol(compiler, curElement); + if (symbol) { + candidates.push(...getSymbolMembers(compiler, symbol)); + } + curElement = curElement instanceof ElementDeclarationNode ? curElement.parent : undefined; + } + + // Walk through the name stack + for (const name of nameStack) { + const matching = candidates.filter((member) => { + const nameResult = member.declaration ? compiler.fullname(member.declaration) : undefined; + const memberName = (nameResult && !nameResult.hasValue(UNHANDLED)) ? nameResult.getValue()?.at(-1) : undefined; + return memberName === name; + }); + if (matching.length === 0) return []; + candidates = matching; + } + + return candidates; +} + function suggestMembers ( compiler: Compiler, offset: number, container: InfixExpressionNode & { op: SyntaxToken }, ): CompletionList { - const fragments = destructureMemberAccessExpression(container).unwrap_or([]); + const fragments = destructureMemberAccessExpression(container) ?? []; fragments.pop(); // The last fragment is not used in suggestions: v1.table.a<> if (fragments.some((f) => !isExpressionAVariableNode(f))) { return noSuggestions(); } - const nameStack = fragments.map((f) => extractVariableFromExpression(f).unwrap()); + const nameStack = fragments.map((f) => extractVariableFromExpression(f)!); + + // Resolve the name stack by walking from the scope's symbol through members + const resolvedSymbols = resolveNameStack(compiler, nameStack, compiler.container.element(offset)); return addQuoteToSuggestionIfNeeded({ - suggestions: compiler.symbol - .ofName(nameStack, compiler.container.element(offset)) - .flatMap(({ symbol }) => compiler.symbol.members(symbol)) - .map(({ kind, name }) => ({ - label: name, - insertText: name, - kind: pickCompletionItemKind(kind), - range: undefined as any, - })), + suggestions: resolvedSymbols + .flatMap((symbol) => getSymbolMembers(compiler, symbol)) + .map((member) => { + const nameResult = member.declaration ? compiler.fullname(member.declaration) : undefined; + const name = (nameResult && !nameResult.hasValue(UNHANDLED)) ? nameResult.getValue()?.at(-1) ?? '' : ''; + return { + label: name, + insertText: name, + kind: pickCompletionItemKind(member.kind), + range: undefined as any, + }; + }) + .filter((s) => s.label !== ''), }); } @@ -697,9 +746,8 @@ function suggestInElementHeader ( offset: number, container: ElementDeclarationNode, ): CompletionList { - const elementKind = getElementKind(container).unwrap_or(undefined); - if (elementKind === ElementKind.Records) { - return suggestNamesInScope(compiler, offset, container.parent, [ + if (container.isKind(ElementKind.Records)) { + return suggestNamesInScope(compiler, offset, container.parent as ElementDeclarationNode | ProgramNode | undefined, [ SymbolKind.Schema, SymbolKind.Table, ]); @@ -721,10 +769,10 @@ function suggestInCallExpression ( // Check if we're in a Records element header (top-level Records) if ( element instanceof ElementDeclarationNode - && getElementKind(element).unwrap_or(undefined) === ElementKind.Records + && element.isKind(ElementKind.Records) && isOffsetWithinElementHeader(offset, element) ) { - if (inCallee) return suggestNamesInScope(compiler, offset, element.parent, [ + if (inCallee) return suggestNamesInScope(compiler, offset, element.parent as ElementDeclarationNode | ProgramNode | undefined, [ SymbolKind.Schema, SymbolKind.Table, ]); @@ -733,9 +781,10 @@ function suggestInCallExpression ( const callee = container.callee; if (!callee) return noSuggestions(); - const fragments = destructureMemberAccessExpression(callee).unwrap_or([callee]); + const fragments = destructureMemberAccessExpression(callee) ?? [callee]; const rightmostExpr = fragments[fragments.length - 1]; - const tableSymbol = rightmostExpr?.referee; + const refereeResult = rightmostExpr ? compiler.nodeReferee(rightmostExpr) : undefined; + const tableSymbol = (refereeResult && !refereeResult.hasValue(UNHANDLED)) ? refereeResult.getValue() : undefined; if (!tableSymbol) return noSuggestions(); const suggestions = suggestMembersOfSymbol(compiler, tableSymbol, [SymbolKind.Column]); @@ -755,8 +804,8 @@ function suggestInCallExpression ( if (!inArgs) continue; if (!(c instanceof FunctionApplicationNode)) continue; if (c.callee !== container) continue; - if (extractVariableFromExpression(container.callee).unwrap_or('').toLowerCase() !== ElementKind.Records) continue; - const tableSymbol = compiler.container.element(offset).symbol; + if ((extractVariableFromExpression(container.callee) ?? '').toLowerCase() !== ElementKind.Records) continue; + const tableSymbol = getNodeSymbol(compiler, compiler.container.element(offset)); if (!tableSymbol) return noSuggestions(); const suggestions = suggestMembersOfSymbol(compiler, tableSymbol, [SymbolKind.Column]); const { argumentList } = container; @@ -769,20 +818,21 @@ function suggestInCallExpression ( } function suggestInTableGroupField (compiler: Compiler): CompletionList { + const publicMembers = compiler.parse.publicSymbolTable() ?? []; return { suggestions: [ ...addQuoteToSuggestionIfNeeded({ - suggestions: [...compiler.parse.publicSymbolTable().entries()].flatMap(([index]) => { - const res = destructureIndex(index).unwrap_or(undefined); - if (res === undefined) return []; - const { kind, name } = res; - if (kind !== SymbolKind.Table && kind !== SymbolKind.Schema) return []; + suggestions: publicMembers.flatMap((member) => { + if (member.kind !== SymbolKind.Table && member.kind !== SymbolKind.Schema) return []; + const nameResult = member.declaration ? compiler.fullname(member.declaration) : undefined; + const name = (nameResult && !nameResult.hasValue(UNHANDLED)) ? nameResult.getValue()?.at(-1) : undefined; + if (!name) return []; return { label: name, insertText: name, insertTextRules: CompletionItemInsertTextRule.KeepWhitespace, - kind: pickCompletionItemKind(kind), + kind: pickCompletionItemKind(member.kind), range: undefined as any, }; }), @@ -881,19 +931,18 @@ function suggestColumnType (compiler: Compiler, offset: number): CompletionList function suggestColumnNameInIndexes (compiler: Compiler, offset: number): CompletionList { const indexesNode = compiler.container.element(offset); const tableNode = (indexesNode as any)?.parent; - if (!(tableNode?.symbol instanceof TableSymbol)) { + const tableSymbol = tableNode ? getNodeSymbol(compiler, tableNode) : undefined; + if (!tableSymbol || !(tableSymbol.isKind(SymbolKind.Table))) { return noSuggestions(); } - const { symbolTable } = tableNode.symbol; + const members = getSymbolMembers(compiler, tableSymbol); return addQuoteToSuggestionIfNeeded({ - suggestions: [...symbolTable.entries()].flatMap(([index]) => { - const res = destructureIndex(index).unwrap_or(undefined); - if (res === undefined) { - return []; - } - const { name } = res; + suggestions: members.flatMap((member) => { + const nameResult = member.declaration ? compiler.fullname(member.declaration) : undefined; + const name = (nameResult && !nameResult.hasValue(UNHANDLED)) ? nameResult.getValue()?.at(-1) : undefined; + if (!name) return []; return { label: name, diff --git a/packages/dbml-parse/src/services/suggestions/recordRowSnippet.ts b/packages/dbml-parse/src/services/suggestions/recordRowSnippet.ts index 743b61141..2903dc0de 100644 --- a/packages/dbml-parse/src/services/suggestions/recordRowSnippet.ts +++ b/packages/dbml-parse/src/services/suggestions/recordRowSnippet.ts @@ -1,15 +1,15 @@ import { - extractReferee, extractVariableFromExpression, - getElementKind, -} from '@/core/analyzer/utils'; +} from '@/core/syntax/utils'; +import { extractReferee, getNodeSymbol } from '@/services/utils'; +import { SymbolKind } from '@/core/types/symbols'; import { BlockExpressionNode, CallExpressionNode, ElementDeclarationNode, ProgramNode, TupleExpressionNode, -} from '@/core/parser/nodes'; +} from '@/core/types/nodes'; import { type CompletionList, type TextModel, @@ -17,15 +17,14 @@ import { CompletionItemKind, CompletionItemInsertTextRule, } from '@/services/types'; -import { ColumnSymbol, TablePartialInjectedColumnSymbol, TableSymbol } from '@/core/analyzer/symbol/symbols'; -import { ElementKind } from '@/core/analyzer/types'; +import { ElementKind } from '@/core/types/keywords'; import Compiler from '@/compiler'; import { noSuggestions, getColumnsFromTableSymbol, extractNameAndTypeOfColumnSymbol, } from '@/services/suggestions/utils'; -import { isOffsetWithinSpan } from '@/core/utils'; +import { isOffsetWithinSpan } from '@/core/utils/span'; export function suggestRecordRowSnippet ( compiler: Compiler, @@ -38,9 +37,8 @@ export function suggestRecordRowSnippet ( // If not in an ElementDeclarationNode, fallthrough if (!(element instanceof ElementDeclarationNode)) return null; - const elementKind = getElementKind(element).unwrap_or(undefined); // If not in a Records element, fallthrough - if (elementKind !== ElementKind.Records || !(element.body instanceof BlockExpressionNode)) return null; + if (!element.isKind(ElementKind.Records) || !(element.body instanceof BlockExpressionNode)) return null; // If we're not within the body, fallthrough if (!element.body || !isOffsetWithinSpan(offset, element.body)) return null; @@ -65,16 +63,16 @@ function suggestRecordRowInTopLevelRecords ( if (!(recordsElement.name instanceof CallExpressionNode)) return noSuggestions(); const columnElements = recordsElement.name.argumentList?.elementList || []; - const columnSymbols = columnElements.map((e) => extractReferee(e)); + const columnSymbols = columnElements.map((e) => extractReferee(compiler, e)); if (!columnSymbols || columnSymbols.length === 0) return noSuggestions(); const columns = columnElements .map((element, index) => { const symbol = columnSymbols[index]; - if (!symbol || !(symbol instanceof ColumnSymbol || symbol instanceof TablePartialInjectedColumnSymbol)) { + if (!symbol || !(symbol.isKind(SymbolKind.Column) || symbol.isKind(SymbolKind.Column))) { return null; } - const columnName = extractVariableFromExpression(element).unwrap_or(undefined); + const columnName = extractVariableFromExpression(element); if (!columnName) return null; const result = extractNameAndTypeOfColumnSymbol(symbol, columnName); return result; @@ -109,8 +107,8 @@ function suggestRecordRowInNestedRecords ( return noSuggestions(); } - const tableSymbol = parent.symbol; - if (!(tableSymbol instanceof TableSymbol)) { + const tableSymbol = getNodeSymbol(compiler, parent); + if (!tableSymbol || !(tableSymbol.isKind(SymbolKind.Table))) { return noSuggestions(); } @@ -120,23 +118,23 @@ function suggestRecordRowInNestedRecords ( // Explicit columns from tuple: records (col1, col2) const columnElements = recordsElement.name.elementList; const columnSymbols = columnElements - .map((e) => extractReferee(e)) + .map((e) => extractReferee(compiler, e)) .filter((s) => s !== undefined); columns = columnElements .map((element, index) => { const symbol = columnSymbols[index]; - if (!symbol || !(symbol instanceof ColumnSymbol || symbol instanceof TablePartialInjectedColumnSymbol)) { + if (!symbol || !(symbol.isKind(SymbolKind.Column) || symbol.isKind(SymbolKind.Column))) { return null; } - const columnName = extractVariableFromExpression(element).unwrap_or(undefined); + const columnName = extractVariableFromExpression(element); if (columnName === undefined) return null; return extractNameAndTypeOfColumnSymbol(symbol, columnName); }) .filter((col) => col !== null) as Array<{ name: string; type: string }>; } else { // Implicit columns - use all columns from parent table - const result = getColumnsFromTableSymbol(tableSymbol); + const result = getColumnsFromTableSymbol(compiler, tableSymbol); if (!result) { return noSuggestions(); } diff --git a/packages/dbml-parse/src/services/suggestions/utils.ts b/packages/dbml-parse/src/services/suggestions/utils.ts index 4407fd108..5865c0a8e 100644 --- a/packages/dbml-parse/src/services/suggestions/utils.ts +++ b/packages/dbml-parse/src/services/suggestions/utils.ts @@ -1,12 +1,13 @@ -import { SymbolKind, destructureIndex, createColumnSymbolIndex } from '@/core/analyzer/symbol/symbolIndex'; +import { SymbolKind, NodeSymbol } from '@/core/types/symbols'; import { CompletionItemKind, CompletionItemInsertTextRule, type CompletionList } from '@/services/types'; -import { SyntaxToken, SyntaxTokenKind } from '@/core/lexer/tokens'; -import { hasTrailingSpaces } from '@/core/lexer/utils'; -import { SyntaxNode, TupleExpressionNode, FunctionApplicationNode } from '@/core/parser/nodes'; +import { SyntaxToken, SyntaxTokenKind } from '@/core/types/tokens'; +import { hasTrailingSpaces } from '@/core/syntax/lexer/utils'; +import { SyntaxNode, TupleExpressionNode, FunctionApplicationNode } from '@/core/types/nodes'; import Compiler from '@/compiler'; -import { ColumnSymbol, TablePartialInjectedColumnSymbol, TablePartialSymbol, TableSymbol } from '@/core/analyzer/symbol/symbols'; -import { extractVariableFromExpression } from '@/core/analyzer/utils'; +import { extractVariableFromExpression } from '@/core/syntax/utils'; import { addDoubleQuoteIfNeeded } from '@/compiler/queries/utils'; +import { getSymbolMembers } from '@/services/utils'; +import { UNHANDLED } from '@/constants'; export function pickCompletionItemKind (symbolKind: SymbolKind): CompletionItemKind { switch (symbolKind) { @@ -140,15 +141,20 @@ export function isTupleEmpty (tuple: TupleExpressionNode): boolean { * @returns Array of column objects with name and type information */ export function getColumnsFromTableSymbol ( - tableSymbol: TableSymbol | TablePartialSymbol, + compiler: Compiler, + tableSymbol: NodeSymbol, ): Array<{ name: string; type: string }> | null { const columns: Array<{ name: string; type: string }> = []; - for (const [index, columnSymbol] of tableSymbol.symbolTable.entries()) { - const res = destructureIndex(index).unwrap_or(undefined); - if (res === undefined || res.kind !== SymbolKind.Column) continue; - if (!(columnSymbol instanceof ColumnSymbol || columnSymbol instanceof TablePartialInjectedColumnSymbol)) continue; - const columnInfo = extractNameAndTypeOfColumnSymbol(columnSymbol, res.name); + const members = getSymbolMembers(compiler, tableSymbol); + if (members.length === 0) return null; + for (const member of members) { + if (!member.isKind(SymbolKind.Column)) continue; + const nameResult = member.declaration ? compiler.fullname(member.declaration) : undefined; + if (!nameResult || nameResult.hasValue(UNHANDLED)) continue; + const columnName = nameResult.getValue()?.at(-1); + if (!columnName) continue; + const columnInfo = extractNameAndTypeOfColumnSymbol(member, columnName); if (!columnInfo) continue; columns.push(columnInfo); } @@ -158,17 +164,14 @@ export function getColumnsFromTableSymbol ( // This function also works with injected columns export function extractNameAndTypeOfColumnSymbol ( - columnSymbol: ColumnSymbol | TablePartialInjectedColumnSymbol, + columnSymbol: NodeSymbol, columnName: string, ): { name: string; type: string } | null { - const columnIndex = createColumnSymbolIndex(columnName); - const columnDeclaration = columnSymbol instanceof TablePartialInjectedColumnSymbol - ? columnSymbol.tablePartialSymbol.symbolTable.get(columnIndex)?.declaration - : columnSymbol.declaration; + const columnDeclaration = columnSymbol.declaration; if (!(columnDeclaration instanceof FunctionApplicationNode)) return null; - const name = extractVariableFromExpression(columnDeclaration.callee).unwrap_or(null); - const type = extractVariableFromExpression(columnDeclaration.args[0]).unwrap_or(null); + const name = extractVariableFromExpression(columnDeclaration.callee) ?? null; + const type = extractVariableFromExpression(columnDeclaration.args[0]) ?? null; if (name === null || type === null) return null; diff --git a/packages/dbml-parse/src/services/utils.ts b/packages/dbml-parse/src/services/utils.ts index b45762f6b..c5dab5e86 100644 --- a/packages/dbml-parse/src/services/utils.ts +++ b/packages/dbml-parse/src/services/utils.ts @@ -1,5 +1,36 @@ +import type Compiler from '@/compiler/index'; +import type { SyntaxNode } from '@/core/types/nodes'; +import { NodeSymbol } from '@/core/types/symbols'; +import { InfixExpressionNode } from '@/core/types/nodes'; import type { TextModel, Position } from '@/services/types'; +import { UNHANDLED } from '@/constants'; +import { nodeSymbol, nodeReferee, symbolMembers } from '@/core/global_modules'; export function getOffsetFromMonacoPosition (model: TextModel, position: Position): number { return model.getOffsetAt(position); } + +// Extract referee from a simple variable (x) or complex variable (a.b.c) +export function extractReferee (compiler: Compiler, node: SyntaxNode | undefined): NodeSymbol | undefined { + if (!node) return undefined; + if (node instanceof InfixExpressionNode && node.op?.value === '.') { + return extractReferee(compiler, node.rightExpression); + } + const result = nodeReferee.call(compiler, node); + if (result.hasValue(UNHANDLED)) return undefined; + return result.getValue() ?? undefined; +} + +// Helper: get symbol for a node, return undefined if unhandled +export function getNodeSymbol (compiler: Compiler, node: SyntaxNode): NodeSymbol | undefined { + const result = nodeSymbol.call(compiler, node); + if (result.hasValue(UNHANDLED)) return undefined; + return result.getValue(); +} + +// Helper: get members of a symbol, return empty array if unhandled +export function getSymbolMembers (compiler: Compiler, symbol: NodeSymbol): NodeSymbol[] { + const result = symbolMembers.call(compiler, symbol); + if (result.hasValue(UNHANDLED)) return []; + return result.getValue(); +}