PHPackages                             butschster/dbml-parser - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. butschster/dbml-parser

ActiveLibrary[Database &amp; ORM](/categories/database)

butschster/dbml-parser
======================

DBML (database markup language) parser written on PHP8.

1.0.0(4mo ago)6416.8k↓21.4%4[3 issues](https://github.com/butschster/dbml-parser/issues)2MITPHPPHP &gt;=8.0CI passing

Since Jun 30Pushed 4mo ago3 watchersCompare

[ Source](https://github.com/butschster/dbml-parser)[ Packagist](https://packagist.org/packages/butschster/dbml-parser)[ RSS](/packages/butschster-dbml-parser/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (5)Dependencies (6)Versions (6)Used By (2)

DBML Parser for PHP
===================

[](#dbml-parser-for-php)

[![Support me on Patreon](https://camo.githubusercontent.com/5ab6d2eb5ab9ab057b7d4438491d6e562eb1808058773fba69e5daea884b32ff/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e742e7376673f75726c3d6874747073253341253246253246736869656c6473696f2d70617472656f6e2e76657263656c2e617070253246617069253346757365726e616d652533446275747363687374657225323674797065253344706174726f6e73267374796c653d666c6174)](https://patreon.com/butschster)[![Latest Stable Version](https://camo.githubusercontent.com/2f5c82d5e7d4b3a2e33031393eafe551041e4cabdb7164a195c20870ddf8bc3b/68747470733a2f2f706f7365722e707567782e6f72672f627574736368737465722f64626d6c2d7061727365722f762f737461626c65)](https://packagist.org/packages/butschster/dbml-parser)[![Build Status](https://github.com/butschster/dbml-parser/actions/workflows/tests.yml/badge.svg)](https://github.com/butschster/dbml-parser/actions/workflows/tests.yml)[![Total Downloads](https://camo.githubusercontent.com/fb5681dd7d18d114a037922a5082970fac694bfb721b7e3e0dc67ee742eed0e4/68747470733a2f2f706f7365722e707567782e6f72672f627574736368737465722f64626d6c2d7061727365722f646f776e6c6f616473)](https://packagist.org/packages/butschster/dbml-parser)[![License](https://camo.githubusercontent.com/9d5a4d7a8fa5f4d95bac144c9e3f46b10ac93694907130d777a3435b7ab48721/68747470733a2f2f706f7365722e707567782e6f72672f627574736368737465722f64626d6c2d7061727365722f6c6963656e7365)](https://packagist.org/packages/butschster/dbml-parser)

[![DBML-parser](https://user-images.githubusercontent.com/773481/125667174-8b349bc0-fb5f-49a2-a651-1cac06bba151.jpg)](https://user-images.githubusercontent.com/773481/125667174-8b349bc0-fb5f-49a2-a651-1cac06bba151.jpg)

A production-ready PHP 8.3+ parser for [DBML (Database Markup Language)](https://www.dbml.org/), transforming human-readable database schemas into structured PHP objects. Generate migrations, ORM entities, documentation, or visualization tools from a single DBML source.

Table of Contents
-----------------

[](#table-of-contents)

- [Why DBML Parser?](#why-dbml-parser)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Complete API Reference](#complete-api-reference)
    - [Schema Operations](#schema-operations)
    - [Project Definition](#project-definition)
    - [Table Operations](#table-operations)
    - [Column Properties](#column-properties)
    - [Index Configuration](#index-configuration)
    - [Enum Management](#enum-management)
    - [Table Groups](#table-groups)
    - [Relationships (Refs)](#relationships-refs)
- [Advanced Usage](#advanced-usage)
- [Use Cases](#use-cases)
- [Error Handling](#error-handling)
- [Contributing](#contributing)
- [Credits](#credits)

Why DBML Parser?
----------------

[](#why-dbml-parser)

DBML (Database Markup Language) is a simple, readable DSL for defining database structures. This parser enables you to:

- **Version Control Database Schemas**: Store your database design in git-friendly text format
- **Generate Code Automatically**: Create ORM entities, migrations, models, and documentation from DBML
- **Visualize Database Design**: Build interactive schema diagrams and documentation sites
- **Share Database Specs**: Communicate database structure with teams using human-readable syntax
- **Build Schema Tools**: Create custom tools for schema validation, transformation, or analysis

This library was inspired by [dbdiagram.io](https://dbdiagram.io/) and built using the powerful [phplrt](https://phplrt.org) parser toolkit.

Installation
------------

[](#installation)

**Requirements:**

- PHP 8.3 or higher
- Composer

Install via Composer:

```
composer require butschster/dbml-parser
```

Quick Start
-----------

[](#quick-start)

Parse a DBML schema and access its components:

```
use Butschster\Dbml\DbmlParserFactory;

// Create parser instance
$parser = DbmlParserFactory::create();

// Parse DBML string
$schema = $parser->parse(getColumns() as $column) {
        echo "  - {$column->getName()}: {$column->getType()->getName()}\n";
    }
}
```

Core Concepts
-------------

[](#core-concepts)

The parser transforms DBML into an Abstract Syntax Tree (AST) with these key node types:

Node TypePurposeExample`SchemaNode`Root container for entire schemaTop-level access to all components`ProjectNode`Project metadata and settingsDatabase type, version, notes`TableNode`Table definition with columns`Table users { ... }``ColumnNode`Column with type and constraints`id int [pk, not null]``IndexNode`Single or composite index`Indexes { (col1, col2) [unique] }``EnumNode`Enum type definition`Enum status { active, inactive }``RefNode`Foreign key relationship`Ref: orders.user_id > users.id``TableGroupNode`Logical grouping of tables`TableGroup core { users, roles }`Complete API Reference
----------------------

[](#complete-api-reference)

### Schema Operations

[](#schema-operations)

The `SchemaNode` is your entry point for accessing all parsed components.

#### Get All Tables

[](#get-all-tables)

```
/** @var \Butschster\Dbml\Ast\SchemaNode $schema */

// Get all tables as array
$tables = $schema->getTables();
// Returns: TableNode[]

foreach ($tables as $table) {
    echo $table->getName() . "\n";
}
```

#### Get Specific Table

[](#get-specific-table)

```
// Check if table exists
if ($schema->hasTable('users')) {
    $table = $schema->getTable('users');
    // Returns: TableNode
}

// Throws TableNotFoundException if not found
try {
    $table = $schema->getTable('nonexistent');
} catch (\Butschster\Dbml\Exceptions\TableNotFoundException $e) {
    // Handle missing table
}
```

#### Access Project Metadata

[](#access-project-metadata)

```
// Check if project is defined
if ($schema->hasProject()) {
    $project = $schema->getProject();
    // Returns: ProjectNode|null
}
```

#### Get Table Groups

[](#get-table-groups)

```
// Get all table groups
$groups = $schema->getTableGroups();
// Returns: TableGroupNode[]

// Check specific group
if ($schema->hasTableGroup('core_tables')) {
    $group = $schema->getTableGroup('core_tables');
    // Returns: TableGroupNode
}

// Throws TableGroupNotFoundException if not found
```

#### Get Enums

[](#get-enums)

```
// Get all enums
$enums = $schema->getEnums();
// Returns: EnumNode[]

// Access specific enum
if ($schema->hasEnum('user_status')) {
    $enum = $schema->getEnum('user_status');
    // Returns: EnumNode
}

// Throws EnumNotFoundException if not found
```

#### Get Relationships

[](#get-relationships)

```
// Get all foreign key relationships
$refs = $schema->getRefs();
// Returns: RefNode[]

foreach ($refs as $ref) {
    $leftTable = $ref->getLeftTable()->getTable();
    $rightTable = $ref->getRightTable()->getTable();
    echo "{$leftTable} references {$rightTable}\n";
}
```

**Complete Schema Example:**

```
use Butschster\Dbml\Ast\SchemaNode;

/** @var SchemaNode $schema */

// Project information
$project = $schema->getProject();
$dbType = $project?->getSetting('database_type')->getValue();

// All components
$allTables = $schema->getTables();        // All table definitions
$allEnums = $schema->getEnums();          // All enum types
$allRefs = $schema->getRefs();            // All relationships
$allGroups = $schema->getTableGroups();   // All table groups

// Component counts
$tableCount = count($allTables);
$enumCount = count($allEnums);
$refCount = count($allRefs);

echo "Database: {$dbType}, Tables: {$tableCount}, Enums: {$enumCount}\n";
```

### Project Definition

[](#project-definition)

The `ProjectNode` stores database-level metadata and configuration.

**DBML Syntax:**

```
Project my_app {
    database_type: 'PostgreSQL'
    note: 'Application database schema'
}

```

#### Access Project Properties

[](#access-project-properties)

```
/** @var \Butschster\Dbml\Ast\ProjectNode $project */
$project = $schema->getProject();

// Get project name
$name = $project->getName();
// Returns: string (e.g., 'my_app')

// Get project note
$note = $project->getNote();
// Returns: string|null

// Get all settings
$settings = $project->getSettings();
// Returns: SettingNode[]
```

#### Access Project Settings

[](#access-project-settings)

```
// Check if setting exists
if ($project->hasSetting('database_type')) {
    $setting = $project->getSetting('database_type');
    // Returns: SettingNode

    $key = $setting->getKey();     // 'database_type'
    $value = $setting->getValue();  // 'PostgreSQL'
}

// Throws ProjectSettingNotFoundException if not found
try {
    $setting = $project->getSetting('unknown_setting');
} catch (\Butschster\Dbml\Exceptions\ProjectSettingNotFoundException $e) {
    // Handle missing setting
}
```

#### Get Node Position

[](#get-node-position)

```
// Get offset in source DBML for debugging
$offset = $project->getOffset();
// Returns: int (character position in parsed string)
```

**Complete Project Example:**

```
use Butschster\Dbml\Ast\ProjectNode;

/** @var ProjectNode $project */

echo "Project: {$project->getName()}\n";
echo "Note: {$project->getNote()}\n\n";

echo "Settings:\n";
foreach ($project->getSettings() as $setting) {
    echo "  {$setting->getKey()}: {$setting->getValue()}\n";
}

// Common settings to check
$dbType = $project->hasSetting('database_type')
    ? $project->getSetting('database_type')->getValue()
    : 'unknown';

$version = $project->hasSetting('version')
    ? $project->getSetting('version')->getValue()
    : null;
```

### Table Operations

[](#table-operations)

The `TableNode` represents a database table with columns, indexes, and relationships.

**DBML Syntax:**

```
Table users as U {
    id int [pk, increment]
    email varchar(255) [unique, not null]
    created_at timestamp

    Note: 'User accounts'

    Indexes {
        email [unique]
        (email, created_at) [name: 'email_created_idx']
    }
}

```

#### Access Table Properties

[](#access-table-properties)

```
/** @var \Butschster\Dbml\Ast\TableNode $table */
$table = $schema->getTable('users');

// Table name
$name = $table->getName();
// Returns: string (e.g., 'users')

// Table alias (from "as U")
$alias = $table->getAlias();
// Returns: string|null (e.g., 'U')

// Table note
$note = $table->getNote();
// Returns: string|null

// Source position
$offset = $table->getOffset();
// Returns: int
```

#### Get Table Columns

[](#get-table-columns)

```
// Get all columns
$columns = $table->getColumns();
// Returns: ColumnNode[] (associative array keyed by column name)

foreach ($columns as $columnName => $column) {
    echo "{$columnName}: {$column->getType()->getName()}\n";
}

// Check if column exists
if ($table->hasColumn('email')) {
    $column = $table->getColumn('email');
    // Returns: ColumnNode
}

// Throws ColumnNotFoundException if not found
try {
    $column = $table->getColumn('nonexistent');
} catch (\Butschster\Dbml\Exceptions\ColumnNotFoundException $e) {
    // Handle missing column
}
```

#### Get Table Indexes

[](#get-table-indexes)

```
// Get all indexes
$indexes = $table->getIndexes();
// Returns: IndexNode[]

foreach ($indexes as $index) {
    $columns = $index->getColumns();
    $indexName = $index->getName();
    $isPrimary = $index->isPrimaryKey();
    $isUnique = $index->isUnique();
}
```

**Complete Table Example:**

```
use Butschster\Dbml\Ast\TableNode;

/** @var TableNode $table */

echo "Table: {$table->getName()}";
if ($alias = $table->getAlias()) {
    echo " (alias: {$alias})";
}
echo "\n";

if ($note = $table->getNote()) {
    echo "Note: {$note}\n";
}

echo "\nColumns:\n";
foreach ($table->getColumns() as $column) {
    $type = $column->getType()->getName();
    $size = $column->getType()->getSize();
    $nullable = $column->isNull() ? 'NULL' : 'NOT NULL';
    $pk = $column->isPrimaryKey() ? '[PK]' : '';

    echo "  {$column->getName()} {$type}";
    if ($size) {echo "({$size})";}
    echo " {$nullable} {$pk}\n";
}

echo "\nIndexes:\n";
foreach ($table->getIndexes() as $index) {
    $indexType = $index->isPrimaryKey() ? 'PRIMARY' : ($index->isUnique() ? 'UNIQUE' : 'INDEX');
    $cols = implode(', ', array_map(fn($c) => $c->getValue(), $index->getColumns()));
    echo "  {$indexType} ({$cols})";
    if ($name = $index->getName()) {echo " [{$name}]";}
    echo "\n";
}
```

### Column Properties

[](#column-properties)

The `ColumnNode` represents a table column with its type, constraints, and metadata.

**DBML Syntax:**

```
Table products {
    id int [pk, increment]
    name varchar(255) [not null]
    price decimal(10,2) [default: 0.00]
    status product_status [not null]
    created_at timestamp [default: `now()`]
    note: 'Product catalog'
}

```

#### Access Basic Properties

[](#access-basic-properties)

```
/** @var \Butschster\Dbml\Ast\Table\ColumnNode $column */
$column = $table->getColumn('price');

// Column name
$name = $column->getName();
// Returns: string (e.g., 'price')

// Source position
$offset = $column->getOffset();
// Returns: int

// Column note
$note = $column->getNote();
// Returns: string|null
```

#### Get Column Type

[](#get-column-type)

```
// Get type information
$type = $column->getType();
// Returns: TypeNode

$typeName = $type->getName();
// Returns: string (e.g., 'decimal', 'varchar', 'int')

$size = $type->getSize();
// Returns: int|null (first size parameter, e.g., 10 from decimal(10,2))

$sizeArray = $type->getSizeArray();
// Returns: int[] (all size parameters, e.g., [10, 2] from decimal(10,2))

$offset = $type->getOffset();
// Returns: int
```

**Type Examples:**

```
// varchar(255)
$type->getName();      // 'varchar'
$type->getSize();      // 255
$type->getSizeArray(); // [255]

// decimal(10,2)
$type->getName();      // 'decimal'
$type->getSize();      // 10
$type->getSizeArray(); // [10, 2]

// int (no size)
$type->getName();      // 'int'
$type->getSize();      // null
$type->getSizeArray(); // []
```

#### Check Column Constraints

[](#check-column-constraints)

```
// Primary key
$isPrimaryKey = $column->isPrimaryKey();
// Returns: bool

// Auto-increment
$isIncrement = $column->isIncrement();
// Returns: bool

// Unique constraint
$isUnique = $column->isUnique();
// Returns: bool

// Nullable
$isNullable = $column->isNull();
// Returns: bool (true = NULL, false = NOT NULL)
```

#### Get Default Value

[](#get-default-value)

```
// Get default value
$default = $column->getDefault();
// Returns: AbstractValue|null

if ($default !== null) {
    $value = $default->getValue();
    // Type depends on value node:
    // - IntNode: int
    // - FloatNode: float
    // - StringNode: string|int|float|bool
    // - BooleanNode: bool
    // - NullNode: null
    // - ExpressionNode: string (e.g., 'now()')
}
```

**Default Value Types:**

```
use Butschster\Dbml\Ast\Values\{IntNode, FloatNode, StringNode, BooleanNode, NullNode, ExpressionNode};

$default = $column->getDefault();

// Check value type
if ($default instanceof IntNode) {
    $intValue = $default->getValue(); // int
}

if ($default instanceof FloatNode) {
    $floatValue = $default->getValue(); // float
}

if ($default instanceof StringNode) {
    $stringValue = $default->getValue(); // string|int|float|bool
}

if ($default instanceof BooleanNode) {
    $boolValue = $default->getValue(); // bool
}

if ($default instanceof NullNode) {
    $nullValue = $default->getValue(); // null
}

if ($default instanceof ExpressionNode) {
    $expression = $default->getValue(); // string (e.g., 'now()')
}
```

#### Get Additional Settings

[](#get-additional-settings)

```
// Get custom settings (beyond standard constraints)
$settings = $column->getSettings();
// Returns: SettingWithValueNode[]

foreach ($settings as $setting) {
    $name = $setting->getName();   // string
    $value = $setting->getValue(); // AbstractValue
}
```

#### Get Column Relationships

[](#get-column-relationships)

```
// Get foreign key references defined inline
$refs = $column->getRefs();
// Returns: RefNode[]

foreach ($refs as $ref) {
    $rightTable = $ref->getRightTable()->getTable();
    $rightColumns = $ref->getRightTable()->getColumns();
}
```

**Complete Column Example:**

```
use Butschster\Dbml\Ast\Table\ColumnNode;

/** @var ColumnNode $column */

// Basic info
echo "Column: {$column->getName()}\n";
echo "Type: {$column->getType()->getName()}";
if ($size = $column->getType()->getSize()) {
    echo "({$size})";
}
echo "\n";

// Constraints
$constraints = [];
if ($column->isPrimaryKey()) {$constraints[] = 'PRIMARY KEY';}
if ($column->isIncrement()) {$constraints[] = 'AUTO_INCREMENT';}
if ($column->isUnique()) {$constraints[] = 'UNIQUE';}
if (!$column->isNull()) {$constraints[] = 'NOT NULL';}

if (!empty($constraints)) {
    echo "Constraints: " . implode(', ', $constraints) . "\n";
}

// Default value
if ($default = $column->getDefault()) {
    $defaultValue = $default->getValue();
    echo "Default: ";
    if ($default instanceof \Butschster\Dbml\Ast\Values\ExpressionNode) {
        echo "`{$defaultValue}`";
    } else {
        echo var_export($defaultValue, true);
    }
    echo "\n";
}

// Note
if ($note = $column->getNote()) {
    echo "Note: {$note}\n";
}

// Relationships
if ($refs = $column->getRefs()) {
    echo "References:\n";
    foreach ($refs as $ref) {
        $table = $ref->getRightTable()->getTable();
        $cols = implode(', ', $ref->getRightTable()->getColumns());
        echo "  -> {$table}({$cols})\n";
    }
}
```

### Index Configuration

[](#index-configuration)

The `IndexNode` represents a table index, which can be single-column or composite.

**DBML Syntax:**

```
Table products {
    id int
    merchant_id int
    status varchar

    Indexes {
        id [pk]
        (merchant_id, status) [name: 'merchant_status_idx', type: hash]
        email [unique]
    }
}

```

#### Access Index Properties

[](#access-index-properties)

```
/** @var \Butschster\Dbml\Ast\Table\IndexNode $index */
$index = $table->getIndexes()[0];

// Index name (optional)
$name = $index->getName();
// Returns: string|null (e.g., 'merchant_status_idx')

// Index type (optional)
$type = $index->getType();
// Returns: string|null (e.g., 'hash', 'btree')

// Index note
$note = $index->getNote();
// Returns: string|null

// Check if primary key
$isPrimary = $index->isPrimaryKey();
// Returns: bool

// Check if unique
$isUnique = $index->isUnique();
// Returns: bool
```

#### Get Indexed Columns

[](#get-indexed-columns)

```
// Get columns in the index
$columns = $index->getColumns();
// Returns: AbstractValue[] (StringNode or ExpressionNode)

foreach ($columns as $column) {
    $columnName = $column->getValue();
    // For StringNode: column name
    // For ExpressionNode: expression like 'LOWER(email)'
}

// Example: Single column index
// Indexes { email [unique] }
$columns = $index->getColumns();
// Returns: [StringNode('email')]

// Example: Composite index
// Indexes { (merchant_id, status) [name: 'idx'] }
$columns = $index->getColumns();
// Returns: [StringNode('merchant_id'), StringNode('status')]

// Example: Expression index
// Indexes { `LOWER(email)` [unique] }
$columns = $index->getColumns();
// Returns: [ExpressionNode('LOWER(email)')]
```

#### Get Custom Settings

[](#get-custom-settings)

```
// Get all settings (including custom ones)
$settings = $index->getSettings();
// Returns: array (mixed setting types)

// Settings can include:
// - PrimaryKeyNode
// - UniqueNode
// - NoteNode
// - SettingWithValueNode (for name, type, and custom settings)
```

**Complete Index Example:**

```
use Butschster\Dbml\Ast\Table\IndexNode;
use Butschster\Dbml\Ast\Values\{StringNode, ExpressionNode};

/** @var IndexNode $index */

// Index type
if ($index->isPrimaryKey()) {
    echo "PRIMARY KEY";
} elseif ($index->isUnique()) {
    echo "UNIQUE INDEX";
} else {
    echo "INDEX";
}

// Index name
if ($name = $index->getName()) {
    echo " {$name}";
}

// Columns
$columnNames = array_map(
    fn($col) => $col->getValue(),
    $index->getColumns()
);
echo " (" . implode(', ', $columnNames) . ")";

// Index type (hash, btree, etc.)
if ($type = $index->getType()) {
    echo " USING {$type}";
}

// Note
if ($note = $index->getNote()) {
    echo "\n  Note: {$note}";
}

echo "\n";

// Determine if composite
$isComposite = count($index->getColumns()) > 1;
echo $isComposite ? "  (Composite index)\n" : "  (Single column)\n";

// Check for expression columns
$hasExpressions = array_reduce(
    $index->getColumns(),
    fn($has, $col) => $has || $col instanceof ExpressionNode,
    false
);
if ($hasExpressions) {
    echo "  (Contains expressions)\n";
}
```

### Enum Management

[](#enum-management)

The `EnumNode` represents an enumeration type that can be used as a column type.

**DBML Syntax:**

```
Enum order_status {
    pending
    processing
    shipped
    delivered [note: 'Order has been delivered to customer']
}

```

#### Access Enum Properties

[](#access-enum-properties)

```
/** @var \Butschster\Dbml\Ast\EnumNode $enum */
$enum = $schema->getEnum('order_status');

// Enum name
$name = $enum->getName();
// Returns: string (e.g., 'order_status')

// Number of values
$count = $enum->count();
// Returns: int (e.g., 4)

// Source position
$offset = $enum->getOffset();
// Returns: int
```

#### Get Enum Values

[](#get-enum-values)

```
// Get all values
$values = $enum->getValues();
// Returns: ValueNode[] (associative array keyed by value name)

foreach ($values as $valueName => $valueNode) {
    echo "{$valueName}: {$valueNode->getValue()}\n";
    if ($note = $valueNode->getNote()) {
        echo "  Note: {$note}\n";
    }
}

// Check if value exists
if ($enum->hasValue('shipped')) {
    $value = $enum->getValue('shipped');
    // Returns: ValueNode
}

// Throws EnumValueNotFoundException if not found
try {
    $value = $enum->getValue('nonexistent');
} catch (\Butschster\Dbml\Exceptions\EnumValueNotFoundException $e) {
    // Handle missing enum value
}
```

#### Access Value Properties

[](#access-value-properties)

```
/** @var \Butschster\Dbml\Ast\Enum\ValueNode $value */
$value = $enum->getValue('delivered');

// Value name
$name = $value->getValue();
// Returns: string (e.g., 'delivered')

// Value note
$note = $value->getNote();
// Returns: string|null

// Source position
$offset = $value->getOffset();
// Returns: int
```

**Complete Enum Example:**

```
use Butschster\Dbml\Ast\EnumNode;
use Butschster\Dbml\Ast\Enum\ValueNode;

/** @var EnumNode $enum */

echo "Enum: {$enum->getName()}\n";
echo "Values: {$enum->count()}\n\n";

// List all values
foreach ($enum->getValues() as $valueName => $value) {
    echo "  - {$valueName}";

    if ($note = $value->getNote()) {
        echo " // {$note}";
    }

    echo "\n";
}

// Check if specific values exist
$requiredValues = ['pending', 'processing', 'completed'];
foreach ($requiredValues as $required) {
    if (!$enum->hasValue($required)) {
        echo "Warning: Missing required value '{$required}'\n";
    }
}

// Generate PHP enum class
echo "\nenum {$enum->getName()}: string {\n";
foreach ($enum->getValues() as $valueName => $value) {
    echo "    case " . strtoupper($valueName) . " = '{$valueName}';\n";
}
echo "}\n";
```

### Table Groups

[](#table-groups)

The `TableGroupNode` represents a logical grouping of related tables.

**DBML Syntax:**

```
TableGroup core_tables {
    users
    roles
    permissions
}

TableGroup e_commerce {
    products
    orders
    order_items
}

```

#### Access Group Properties

[](#access-group-properties)

```
/** @var \Butschster\Dbml\Ast\TableGroupNode $group */
$group = $schema->getTableGroup('core_tables');

// Group name
$name = $group->getName();
// Returns: string (e.g., 'core_tables')

// Source position
$offset = $group->getOffset();
// Returns: int
```

#### Get Group Tables

[](#get-group-tables)

```
// Get all table names in group
$tables = $group->getTables();
// Returns: string[] (array of table names)

foreach ($tables as $tableName) {
    if ($schema->hasTable($tableName)) {
        $table = $schema->getTable($tableName);
        // Process table
    }
}

// Check if specific table is in group
if ($group->hasTable('users')) {
    echo "users table is in {$group->getName()} group\n";
}
```

**Complete Table Group Example:**

```
use Butschster\Dbml\Ast\TableGroupNode;
use Butschster\Dbml\Ast\SchemaNode;

/** @var TableGroupNode $group */
/** @var SchemaNode $schema */

echo "Table Group: {$group->getName()}\n";
echo "Tables (" . count($group->getTables()) . "):\n";

foreach ($group->getTables() as $tableName) {
    echo "  - {$tableName}";

    if ($schema->hasTable($tableName)) {
        $table = $schema->getTable($tableName);
        $columnCount = count($table->getColumns());
        echo " ({$columnCount} columns)";
    } else {
        echo " [TABLE NOT FOUND]";
    }

    echo "\n";
}

// Group tables by their groups
$allGroups = $schema->getTableGroups();
$groupedTables = [];

foreach ($allGroups as $grp) {
    foreach ($grp->getTables() as $tableName) {
        $groupedTables[$tableName][] = $grp->getName();
    }
}

// Find tables in multiple groups
foreach ($groupedTables as $tableName => $groups) {
    if (count($groups) > 1) {
        echo "Table '{$tableName}' is in multiple groups: " . implode(', ', $groups) . "\n";
    }
}
```

### Relationships (Refs)

[](#relationships-refs)

The `RefNode` represents a foreign key relationship between tables.

**DBML Syntax:**

```
// Inline relationship
Table orders {
    user_id int [ref: > users.id]
}

// Standalone relationship (short form)
Ref: orders.user_id > users.id

// Standalone relationship (long form with actions)
Ref order_user_fk: products.merchant_id > merchants.id [
    delete: cascade,
    update: no action
]

// Composite foreign key
Ref: order_items.(order_id, product_id) > orders.(id, product_id)

// Relationship types:
// >  : many-to-one
// <  : one-to-many
// -  : one-to-one

// Grouped relationships
Ref {
    products.category_id > categories.id
    products.merchant_id > merchants.id
}

```

#### Access Relationship Properties

[](#access-relationship-properties)

```
/** @var \Butschster\Dbml\Ast\RefNode $ref */
$ref = $schema->getRefs()[0];

// Relationship name (optional)
$name = $ref->getName();
// Returns: string|null (e.g., 'order_user_fk')

// Source position
$offset = $ref->getOffset();
// Returns: int
```

#### Get Relationship Type

[](#get-relationship-type)

```
// Get relationship type
$type = $ref->getType();
// Returns: TypeNode (one of the following)

use Butschster\Dbml\Ast\Ref\Type\{ManyToOneNode, OneToManyNode, OneToOneNode};

if ($type instanceof ManyToOneNode) {
    echo "Many-to-One (>)\n";
} elseif ($type instanceof OneToManyNode) {
    echo "One-to-Many (getAction()}\n";
}

// Check if action exists
if ($ref->hasAction('delete')) {
    $action = $ref->getAction('delete');
    // Returns: OnDeleteNode

    $actionType = $action->getAction();
    // Returns: string (cascade, restrict, set null, set default, no action)
}

// Throws RefActionNotFoundException if not found
try {
    $action = $ref->getAction('nonexistent');
} catch (\Butschster\Dbml\Exceptions\RefActionNotFoundException $e) {
    // Handle missing action
}
```

**Action Types:**

ActionDescription`cascade`Automatically update/delete related rows`restrict`Prevent action if related rows exist`set null`Set foreign key to NULL`set default`Set foreign key to default value`no action`No automatic action (similar to restrict)**Action Node Types:**

```
use Butschster\Dbml\Ast\Ref\Action\{OnDeleteNode, OnUpdateNode};

// Check action type
if ($action instanceof OnDeleteNode) {
    echo "ON DELETE {$action->getAction()}\n";
}

if ($action instanceof OnUpdateNode) {
    echo "ON UPDATE {$action->getAction()}\n";
}

// Both extend ActionNode
$actionName = $action->getName(); // 'delete' or 'update'
$actionType = $action->getAction(); // 'cascade', 'restrict', etc.
```

**Complete Relationship Example:**

```
use Butschster\Dbml\Ast\RefNode;
use Butschster\Dbml\Ast\Ref\Type\{ManyToOneNode, OneToManyNode, OneToOneNode};

/** @var RefNode $ref */

// Relationship name
if ($name = $ref->getName()) {
    echo "Relationship: {$name}\n";
}

// Tables and columns
$left = $ref->getLeftTable();
$right = $ref->getRightTable();

echo "{$left->getTable()}." . implode(', ', $left->getColumns());

// Relationship type
$type = $ref->getType();
if ($type instanceof ManyToOneNode) {
    echo " > ";
} elseif ($type instanceof OneToManyNode) {
    echo " < ";
} else {
    echo " - ";
}

echo "{$right->getTable()}." . implode(', ', $right->getColumns());
echo "\n";

// Actions
if ($actions = $ref->getActions()) {
    echo "Actions:\n";
    foreach ($actions as $actionName => $action) {
        echo "  ON " . strtoupper($actionName) . " {$action->getAction()}\n";
    }
}

// Generate SQL
$leftCols = implode(', ', $left->getColumns());
$rightCols = implode(', ', $right->getColumns());

echo "\nSQL:\n";
echo "ALTER TABLE {$left->getTable()}\n";
echo "  ADD CONSTRAINT " . ($name ?: "fk_{$left->getTable()}_{$right->getTable()}") . "\n";
echo "  FOREIGN KEY ({$leftCols})\n";
echo "  REFERENCES {$right->getTable()}({$rightCols})";

if ($ref->hasAction('delete')) {
    echo "\n  ON DELETE " . strtoupper($ref->getAction('delete')->getAction());
}
if ($ref->hasAction('update')) {
    echo "\n  ON UPDATE " . strtoupper($ref->getAction('update')->getAction());
}
echo ";\n";
```

Advanced Usage
--------------

[](#advanced-usage)

### Validating Schema Integrity

[](#validating-schema-integrity)

```
use Butschster\Dbml\Ast\SchemaNode;

function validateSchema(SchemaNode $schema): array
{
    $errors = [];

    // Check for orphaned foreign keys
    foreach ($schema->getRefs() as $ref) {
        $leftTable = $ref->getLeftTable()->getTable();
        $rightTable = $ref->getRightTable()->getTable();

        if (!$schema->hasTable($leftTable)) {
            $errors[] = "Reference from non-existent table: {$leftTable}";
        }

        if (!$schema->hasTable($rightTable)) {
            $errors[] = "Reference to non-existent table: {$rightTable}";
        }

        // Validate columns exist
        if ($schema->hasTable($leftTable)) {
            $table = $schema->getTable($leftTable);
            foreach ($ref->getLeftTable()->getColumns() as $col) {
                if (!$table->hasColumn($col)) {
                    $errors[] = "Column {$leftTable}.{$col} not found";
                }
            }
        }
    }

    // Check for duplicate column names
    foreach ($schema->getTables() as $table) {
        $columnNames = array_map(fn($c) => $c->getName(), $table->getColumns());
        $duplicates = array_filter(
            array_count_values($columnNames),
            fn($count) => $count > 1
        );

        foreach ($duplicates as $colName => $count) {
            $errors[] = "Duplicate column '{$colName}' in table {$table->getName()}";
        }
    }

    return $errors;
}

$errors = validateSchema($schema);
if (!empty($errors)) {
    foreach ($errors as $error) {
        echo "Error: {$error}\n";
    }
}
```

### Generating Documentation

[](#generating-documentation)

```
use Butschster\Dbml\Ast\SchemaNode;

function generateMarkdownDocs(SchemaNode $schema): string
{
    $md = "# Database Schema\n\n";

    if ($project = $schema->getProject()) {
        $md .= "**Project:** {$project->getName()}\n\n";

        if ($note = $project->getNote()) {
            $md .= "> {$note}\n\n";
        }
    }

    // Tables
    $md .= "## Tables\n\n";
    foreach ($schema->getTables() as $table) {
        $md .= "### {$table->getName()}\n\n";

        if ($note = $table->getNote()) {
            $md .= "*{$note}*\n\n";
        }

        // Columns table
        $md .= "| Column | Type | Constraints |\n";
        $md .= "|--------|------|-------------|\n";

        foreach ($table->getColumns() as $column) {
            $type = $column->getType()->getName();
            if ($size = $column->getType()->getSize()) {
                $type .= "({$size})";
            }

            $constraints = [];
            if ($column->isPrimaryKey()) {$constraints[] = 'PK';}
            if ($column->isUnique()) {$constraints[] = 'UNIQUE';}
            if (!$column->isNull()) {$constraints[] = 'NOT NULL';}
            if ($column->isIncrement()) {$constraints[] = 'AUTO_INCREMENT';}

            $md .= "| {$column->getName()} | {$type} | " . implode(', ', $constraints) . " |\n";
        }

        $md .= "\n";
    }

    // Enums
    if (!empty($schema->getEnums())) {
        $md .= "## Enums\n\n";
        foreach ($schema->getEnums() as $enum) {
            $md .= "### {$enum->getName()}\n\n";
            foreach ($enum->getValues() as $value) {
                $md .= "- `{$value->getValue()}`";
                if ($note = $value->getNote()) {
                    $md .= " - {$note}";
                }
                $md .= "\n";
            }
            $md .= "\n";
        }
    }

    return $md;
}

$markdown = generateMarkdownDocs($schema);
file_put_contents('schema.md', $markdown);
```

### Converting to Different Formats

[](#converting-to-different-formats)

```
use Butschster\Dbml\Ast\SchemaNode;

function convertToArray(SchemaNode $schema): array
{
    $result = [];

    // Project
    if ($project = $schema->getProject()) {
        $result['project'] = [
            'name' => $project->getName(),
            'note' => $project->getNote(),
            'settings' => array_map(
                fn($s) => ['key' => $s->getKey(), 'value' => $s->getValue()],
                $project->getSettings()
            ),
        ];
    }

    // Tables
    $result['tables'] = [];
    foreach ($schema->getTables() as $table) {
        $result['tables'][$table->getName()] = [
            'alias' => $table->getAlias(),
            'note' => $table->getNote(),
            'columns' => array_map(function($col) {
                return [
                    'name' => $col->getName(),
                    'type' => $col->getType()->getName(),
                    'size' => $col->getType()->getSizeArray(),
                    'primary_key' => $col->isPrimaryKey(),
                    'unique' => $col->isUnique(),
                    'nullable' => $col->isNull(),
                    'increment' => $col->isIncrement(),
                    'default' => $col->getDefault()?->getValue(),
                    'note' => $col->getNote(),
                ];
            }, $table->getColumns()),
            'indexes' => array_map(function($idx) {
                return [
                    'name' => $idx->getName(),
                    'columns' => array_map(fn($c) => $c->getValue(), $idx->getColumns()),
                    'primary_key' => $idx->isPrimaryKey(),
                    'unique' => $idx->isUnique(),
                    'type' => $idx->getType(),
                ];
            }, $table->getIndexes()),
        ];
    }

    // Enums
    $result['enums'] = [];
    foreach ($schema->getEnums() as $enum) {
        $result['enums'][$enum->getName()] = array_map(
            fn($v) => ['value' => $v->getValue(), 'note' => $v->getNote()],
            $enum->getValues()
        );
    }

    // Relationships
    $result['relationships'] = array_map(function($ref) {
        $type = match (get_class($ref->getType())) {
            \Butschster\Dbml\Ast\Ref\Type\ManyToOneNode::class => 'many-to-one',
            \Butschster\Dbml\Ast\Ref\Type\OneToManyNode::class => 'one-to-many',
            \Butschster\Dbml\Ast\Ref\Type\OneToOneNode::class => 'one-to-one',
        };

        return [
            'name' => $ref->getName(),
            'type' => $type,
            'from' => [
                'table' => $ref->getLeftTable()->getTable(),
                'columns' => $ref->getLeftTable()->getColumns(),
            ],
            'to' => [
                'table' => $ref->getRightTable()->getTable(),
                'columns' => $ref->getRightTable()->getColumns(),
            ],
            'actions' => array_map(
                fn($a) => ['name' => $a->getName(), 'action' => $a->getAction()],
                $ref->getActions()
            ),
        ];
    }, $schema->getRefs());

    return $result;
}

// Convert to JSON
$json = json_encode(convertToArray($schema), JSON_PRETTY_PRINT);
file_put_contents('schema.json', $json);

// Convert to YAML
$yaml = yaml_emit(convertToArray($schema));
file_put_contents('schema.yaml', $yaml);
```

### Generating Migration Code

[](#generating-migration-code)

```
use Butschster\Dbml\Ast\{SchemaNode, TableNode};

function generateLaravelMigration(TableNode $table): string
{
    $className = 'Create' . str_replace('_', '', ucwords($table->getName(), '_')) . 'Table';
    $tableName = $table->getName();

    $code = "
