DBML — Elixir Parser and Code Generator

buildHex.pmHex.pm

A complete Elixir implementation for parsing Database Markup Language (DBML) schemas and generating Ecto schema files and Ecto migrations automatically.

Features:

Table of Contents


Installation

Add dbml to your dependencies in mix.exs:

def deps do
  [
    {:dbml, "~> 0.3"}
  ]
end

Then run:

mix deps.get

Quick Start

Parse a DBML file

{:ok, tokens} = DBML.parse_file("schema.dbml")

Generate Ecto schemas

{:ok, tokens} = DBML.parse_file("schema.dbml")

DBML.generate_ecto_schemas(tokens, "lib/my_app/schema", namespace: "MyApp.Schema")
# Output: lib/my_app/schema/users.ex, lib/my_app/schema/posts.ex, ...

Generate Ecto migrations

DBML.generate_ecto_migrations(
  tokens,
  "priv/repo/migrations",
  "MyApp.Repo"
)
# Output: priv/repo/migrations/20000101000001_create_users.exs, ...

CLI Usage (Escript & Mix Tasks)

The DBML library includes both a standalone escript binary and Mix tasks for convenient command-line usage.

Building the escript

mix escript.build
# Produces: ./dbml (standalone executable)

Using the dbml escript

# Generate Ecto schemas from DBML
./dbml schemas schema.dbml -o lib/my_app/schema --namespace MyApp.Schema

# Generate Ecto schemas, overwriting existing files
./dbml schemas schema.dbml -o lib/my_app/schema --overwrite

# Generate Ecto migrations from DBML
./dbml migrations schema.dbml -o priv/repo/migrations -r MyApp.Repo

# Generate Ecto migrations, overwriting existing files
./dbml migrations schema.dbml -o priv/repo/migrations -r MyApp.Repo --overwrite

# Generate DBML from existing Ecto schemas
./dbml file lib/my_app/schema -o schema.dbml --project-name MyApp

# Show help
./dbml help
./dbml schemas --help
./dbml migrations --help
./dbml file --help

Using Mix tasks

The same functionality is available as Mix tasks for use during development:

# Generate Ecto schemas from DBML
mix dbml.schemas schema.dbml -o lib/my_app/schema --namespace MyApp.Schema

# Generate Ecto schemas, overwriting existing files
mix dbml.schemas schema.dbml -o lib/my_app/schema --overwrite

# Generate Ecto migrations from DBML
mix dbml.migrations schema.dbml -o priv/repo/migrations -r MyApp.Repo

# Generate Ecto migrations, overwriting existing files
mix dbml.migrations schema.dbml -o priv/repo/migrations -r MyApp.Repo --overwrite

# Generate DBML from existing Ecto schemas
mix dbml.file lib/my_app/schema -o schema.dbml --project-name MyApp

# Show help for each task
mix help dbml.schemas
mix help dbml.migrations
mix help dbml.file

Escript options

dbml schemas <DBML_FILE> [OPTIONS]

Generate Ecto schema files from a DBML schema.

-o, --output-dir DIR         Output directory for .ex files (required)
-n, --namespace MODULE       Module namespace prefix (optional)
--singularize true|false     Singularize table names (default: true)
--update true|false          Allow overwriting existing files (default: false)
--overwrite true|false       Allow overwriting existing files (default: false)

dbml migrations <DBML_FILE> [OPTIONS]

Generate Ecto migration files from a DBML schema.

-o, --output-dir DIR         Output directory for migration files (required)
-r, --repo MODULE            Repo module name (required)
--base-timestamp TIMESTAMP   Base timestamp for migrations (default: 20000101000000)
--update true|false          Allow incremental updates (default: false)
--overwrite true|false       Allow overwriting existing files (default: false)

dbml file <SCHEMAS_DIR> [OPTIONS]

Generate a DBML file from existing Ecto schema files (reverse operation).

-o, --output FILE            Output path for the .dbml file (required)
--project-name NAME          Project name for DBML header (optional)
--database-type TYPE         Database type (default: PostgreSQL)

Parsing DBML

Parse from string

dbml_string = """
table users {
  id int [pk]
  email varchar [unique, not null]
  created_at timestamp
}
"""

{:ok, tokens} = DBML.parse(dbml_string)

Parse from file

{:ok, tokens} = DBML.parse_file("schema.dbml")

Error handling

case DBML.parse(invalid_dbml) do
  {:ok, tokens} -> process_tokens(tokens)
  {:error, reason} -> handle_error(reason)
end

Generating Ecto Schemas

Basic usage

{:ok, tokens} = DBML.parse_file("schema.dbml")

# Generate with default namespace (extracted from DBML project name, or empty)
{:ok, paths} = DBML.generate_ecto_schemas(tokens, "lib/my_app/schema")

# Generate with explicit namespace
{:ok, paths} = DBML.generate_ecto_schemas(
  tokens,
  "lib/my_app/schema",
  namespace: "MyApp.Schema"
)

Options

:namespace (string, optional)

Module namespace prefix for generated schemas.

Default: Extracted from DBML project { name: "..." }, or empty string "".

{:ok, paths} = DBML.generate_ecto_schemas(
  tokens,
  "lib/my_app/schema",
  namespace: "MyApp.Schema"
)
# Generates: MyApp.Schema.User, MyApp.Schema.Post, etc.

:singularize (boolean, default: true)

Convert plural table names to singular module names.

# With singularize: true (default)
# Table: users → Module: User
# Table: posts → Module: Post

# With singularize: false
{:ok, paths} = DBML.generate_ecto_schemas(
  tokens,
  "lib/my_app/schema",
  singularize: false
)
# Table: users → Module: Users
# Table: posts → Module: Posts

:update (boolean, default: false)

Control behavior when schema files already exist.

# update: false (default) — error if any file exists
{:error, "File already exists: lib/my_app/schema/users.ex"} =
  DBML.generate_ecto_schemas(tokens, "lib/my_app/schema")

# update: true — overwrite existing files
{:ok, paths} = DBML.generate_ecto_schemas(
  tokens,
  "lib/my_app/schema",
  update: true
)

Return value

Generated schema features

The generator creates full Ecto schemas with:


Generating Ecto Migrations

Basic usage

{:ok, tokens} = DBML.parse_file("schema.dbml")

# Generate migrations
{:ok, paths} = DBML.generate_ecto_migrations(
  tokens,
  "priv/repo/migrations",
  "MyApp.Repo"
)

Options

:base_timestamp (integer, default: 20000101000000)

Base timestamp for migration file naming. Each table gets base + index.

{:ok, paths} = DBML.generate_ecto_migrations(
  tokens,
  "priv/repo/migrations",
  "MyApp.Repo",
  base_timestamp: 20_250_101_000_000
)
# Generates: 20250101000001_create_users.exs, 20250101000002_create_posts.exs, ...

:update (boolean, default: false)

Control incremental migration generation when schemas change.

update: false (default):

{:error, "File already exists: priv/repo/migrations/20000101000001_create_users.exs"} =
  DBML.generate_ecto_migrations(tokens, "priv/repo/migrations", "MyApp.Repo")

update: true:

# First generation
{:ok, ["/priv/repo/migrations/20000101000001_create_users.exs"]} =
  DBML.generate_ecto_migrations(tokens, "priv/repo/migrations", "MyApp.Repo", update: true)

# Schema updated: add email column to users, add posts table
{:ok, paths} =
  DBML.generate_ecto_migrations(new_tokens, "priv/repo/migrations", "MyApp.Repo", update: true)

# Returns: [
#   "/priv/repo/migrations/20000101000002_create_users.exs",    # changed (new migration)
#   "/priv/repo/migrations/20000101000002_create_posts.exs"     # new table
# ]

# Old migration still exists:
# /priv/repo/migrations/20000101000001_create_users.exs (unchanged)

Return value

Generated migration features

The generator creates Ecto migrations with:

Migration versioning workflow

Migrations are append-only and audit-preserving:

# Day 1: Create initial schema
schema_v1 = """
table users {
  id int [pk]
  name varchar
}
"""

{:ok, tokens_v1} = DBML.parse(schema_v1)
{:ok, [users_mig]} =
  DBML.generate_ecto_migrations(tokens_v1, "priv/repo/migrations", "MyApp.Repo")
# Creates: 20000101000001_create_users.exs

# Day 2: Add column to users, add posts table
schema_v2 = """
table users {
  id int [pk]
  name varchar
  email varchar
}

table posts {
  id int [pk]
  user_id int [ref: > users.id]
  title varchar
}
"""

{:ok, tokens_v2} = DBML.parse(schema_v2)
{:ok, new_migs} =
  DBML.generate_ecto_migrations(tokens_v2, "priv/repo/migrations", "MyApp.Repo", update: true)
# Creates: 20000101000002_create_users.exs (users changed, new migration)
#          20000101000002_create_posts.exs (posts is new)
# Keeps:   20000101000001_create_users.exs (original, unchanged)

# Database migration history preserved:
# → 20000101000001_create_users.exs
# → 20000101000002_create_users.exs  (alter users table)
# → 20000101000002_create_posts.exs  (create posts table)

Generating DBML from Ecto Schemas

Generate a DBML schema file from existing Ecto schema files. This is the reverse operation: instead of parsing DBML and generating schemas, read Ecto schemas and produce DBML.

Basic usage

# Generate DBML from all .ex files in a directory
{:ok, output_path} = DBML.schemas_to_dbml(
  "lib/my_app/schema",
  "schema.dbml"
)

With options

{:ok, output_path} = DBML.schemas_to_dbml(
  "lib/my_app/schema",
  "schema.dbml",
  project_name: "MyApp",
  database_type: "PostgreSQL"
)

Options

:project_name (string, optional)

Adds a project block to the generated DBML with metadata.

{:ok, output_path} = DBML.schemas_to_dbml(
  "lib/my_app/schema",
  "schema.dbml",
  project_name: "MyApp"
)

# Generates:
# project MyApp {
#   database_type: "PostgreSQL"
# }

:database_type (string, default: "PostgreSQL")

Sets the database type in the project block.

{:ok, output_path} = DBML.schemas_to_dbml(
  "lib/my_app/schema",
  "schema.dbml",
  project_name: "MyApp",
  database_type: "MySQL"
)

Supported Ecto features

The generator reads Ecto schemas and extracts:

Example

Input schema files:

# lib/my_app/schema/user.ex
defmodule MyApp.Schema.User do
  use Ecto.Schema

  schema "users" do
    field :email, :string, null: false, unique: true
    field :status, Ecto.Enum, values: [:active, :inactive, :pending]
    timestamps()
  end
end

# lib/my_app/schema/post.ex
defmodule MyApp.Schema.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string, null: false
    field :content, :string
    belongs_to :user, MyApp.Schema.User
    timestamps()
  end
end

Generation:

{:ok, path} = DBML.schemas_to_dbml(
  "lib/my_app/schema",
  "schema.dbml",
  project_name: "MyApp"
)

Output DBML:

project MyApp {
  database_type: "PostgreSQL"
}

enum status {
  active
  inactive
  pending
}

table users {
  id int [pk]
  email varchar [not null, unique]
  status status
  created_at timestamp
  updated_at timestamp
}

table posts {
  id int [pk]
  title varchar [not null]
  content varchar
  user_id int [ref: > users.id]
  created_at timestamp
  updated_at timestamp
}

ref: posts.user_id > users.id

Return value

Use cases

Visualize existing schemas: Generate DBML from production Ecto schemas and visualize them in dbdiagram.io.

Document schemas: Keep a DBML representation of your database schema in version control.

Migrate between tools: Convert Ecto schemas to DBML, then use DBML tools for schema design and visualization.

Round-trip testing: Parse DBML → generate schemas → read schemas → generate DBML to verify round-trip fidelity.


Type Mapping

DBML to Ecto Type Mapping

DBML Type Ecto Type Notes
int, integer, smallint, bigint:integer
varchar, char, text, character varying:string
boolean, bool:boolean
float, double, real:float
decimal, numeric:decimal
date:date
datetime, timestamp, timestamptz:datetime
time:time
uuid:uuid Binary UUID type
json, jsonb:map
serial, bigserial:integer Auto-increment in migrations

Enum mapping

DBML enums are converted to Ecto.Enum fields:

enum product_status {
  out_of_stock
  in_stock
  running_low
}

table products {
  id int [pk]
  status product_status
}

Generates:

defmodule MyApp.Product do
  use Ecto.Schema

  schema "products" do
    field :status, Ecto.Enum, values: [:out_of_stock, :in_stock, :running_low]
  end
end

Examples

Complete schema example

schema.dbml:

project CMS {
  database_type: "PostgreSQL"
  note: "Content management system"
}

enum post_status {
  draft
  published
  archived
}

table users {
  id int [pk, increment]
  email varchar [unique, not null]
  full_name varchar
  created_at timestamp
  updated_at timestamp
}

table posts {
  id int [pk, increment]
  user_id int [ref: > users.id, not null]
  title varchar [not null]
  content text
  status post_status [default: 'draft']
  published_at datetime
  created_at timestamp
  updated_at timestamp

  indexes {
    (user_id, status)
    published_at [type: btree]
  }
}

table comments {
  id int [pk, increment]
  post_id int [ref: > posts.id, not null]
  user_id int [ref: > users.id, not null]
  content text [not null]
  created_at timestamp
  updated_at timestamp
}

Generate schemas:

{:ok, tokens} = DBML.parse_file("schema.dbml")

{:ok, paths} = DBML.generate_ecto_schemas(
  tokens,
  "lib/cms/schema",
  namespace: "CMS.Schema"
)

# Creates:
# - lib/cms/schema/users.ex
# - lib/cms/schema/posts.ex
# - lib/cms/schema/comments.ex

Generate migrations:

{:ok, paths} = DBML.generate_ecto_migrations(
  tokens,
  "priv/repo/migrations",
  "CMS.Repo"
)

# Creates:
# - priv/repo/migrations/20000101000001_create_users.exs
# - priv/repo/migrations/20000101000002_create_posts.exs
# - priv/repo/migrations/20000101000003_create_comments.exs
# (topological order: users first, then posts, then comments)

Handling schema evolution

# Initial schema
initial_schema = DBML.parse_file("schema.dbml")

# Generate all artifacts
DBML.generate_ecto_schemas(
  initial_schema,
  "lib/my_app/schema",
  namespace: "MyApp",
  update: false  # fail if files exist
)

DBML.generate_ecto_migrations(
  initial_schema,
  "priv/repo/migrations",
  "MyApp.Repo",
  update: false  # fail if files exist
)

# ... run migrations, commit ...

# Later: schema evolves
evolved_schema = DBML.parse_file("schema.dbml")

# Update schemas safely (only changed files rewritten)
DBML.generate_ecto_schemas(
  evolved_schema,
  "lib/my_app/schema",
  namespace: "MyApp",
  update: true  # overwrite changed, skip unchanged
)

# Update migrations intelligently (append-only)
DBML.generate_ecto_migrations(
  evolved_schema,
  "priv/repo/migrations",
  "MyApp.Repo",
  update: true  # new files for changed tables, skip unchanged
)

API Reference

DBML module

parse(dbml_string) :: {:ok, tokens} | {:error, reason}

Parse a DBML schema from a string.

Parameters:

Returns:

{:ok, tokens} = DBML.parse("""
  table users {
    id int [pk]
  }
""")

parse_file(filepath) :: {:ok, tokens} | {:error, reason}

Parse a DBML schema from a file.

Parameters:

Returns:

{:ok, tokens} = DBML.parse_file("priv/schema.dbml")

generate_ecto_schemas(tokens, output_dir, opts) :: {:ok, paths} | {:error, reason}

Generate Ecto schema files from parsed DBML tokens.

Parameters:

Options:

Returns:

{:ok, paths} = DBML.generate_ecto_schemas(
  tokens,
  "lib/my_app/schema",
  namespace: "MyApp.Schema",
  singularize: true,
  update: false
)

generate_ecto_migrations(tokens, output_dir, repo_module, opts) :: {:ok, paths} | {:error, reason}

Generate Ecto migration files from parsed DBML tokens.

Parameters:

Options:

Returns:

{:ok, paths} = DBML.generate_ecto_migrations(
  tokens,
  "priv/repo/migrations",
  "MyApp.Repo",
  base_timestamp: 20_250_101_000_000,
  update: true
)

schemas_to_dbml(input_dir, output_path, opts) :: {:ok, output_path} | {:error, reason}

Generate a DBML schema file from existing Ecto schema files.

Reads all *.ex files in input_dir that contain use Ecto.Schema, parses their structure, and writes a single .dbml file to output_path.

Parameters:

Options:

Returns:

{:ok, path} = DBML.schemas_to_dbml(
  "lib/my_app/schema",
  "schema.dbml",
  project_name: "MyApp",
  database_type: "PostgreSQL"
)

DBML Syntax Reference

For complete DBML syntax documentation, see the official DBML Documentation.

Quick reference

project ProjectName {
  database_type: "PostgreSQL"
  note: "Project description"
}

enum status_enum {
  active
  inactive
  pending
}

table users as U {
  id int [pk, increment]
  email varchar [unique, not null]
  name varchar
  status status_enum [default: 'active']
  created_at timestamp
  updated_at timestamp

  note: "User accounts table"

  indexes {
    email [unique]
    (status, created_at)
  }
}

table posts {
  id int [pk, increment]
  user_id int [ref: > U.id, not null]
  title varchar [not null]
  content text

  indexes {
    user_id
  }
}

ref: users.id < posts.user_id [comment: "One user has many posts"]

Common Use Cases

For a new Phoenix project (using CLI)

# 1. Create your DBML schema
cat > priv/schema.dbml << 'EOF'
table users {
  id int [pk]
  email varchar [unique, not null]
  created_at timestamp
  updated_at timestamp
}
EOF

# 2. Build the escript (or use mix tasks)
mix escript.build

# 3. Generate migrations
./dbml migrations priv/schema.dbml -o priv/repo/migrations -r MyApp.Repo

# 4. Generate schemas
./dbml schemas priv/schema.dbml -o lib/my_app/schema --namespace MyApp

# 5. Run migrations
mix ecto.migrate

Alternatively, use Mix tasks:

mix dbml.migrations priv/schema.dbml -o priv/repo/migrations -r MyApp.Repo
mix dbml.schemas priv/schema.dbml -o lib/my_app/schema --namespace MyApp
mix ecto.migrate

For a new Phoenix project (using Elixir)

# 1. Create your DBML schema (same as above)

# 2. In iex:
iex> {:ok, tokens} = DBML.parse_file("priv/schema.dbml")
iex> DBML.generate_ecto_migrations(tokens, "priv/repo/migrations", "MyApp.Repo")
iex> DBML.generate_ecto_schemas(tokens, "lib/my_app/schema", namespace: "MyApp")

# 3. Run migrations
mix ecto.migrate

For an existing project (using CLI)

# 1. Edit your DBML schema
# vim priv/schema.dbml

# 2. Regenerate using the escript (or mix tasks)
./dbml schemas priv/schema.dbml -o lib/my_app/schema --namespace MyApp --update true
./dbml schemas priv/schema.dbml -o lib/my_app/schema --namespace MyApp --overwrite
./dbml migrations priv/schema.dbml -o priv/repo/migrations -r MyApp.Repo --update true
./dbml migrations priv/schema.dbml -o priv/repo/migrations -r MyApp.Repo --overwrite

# 3. Review and run new migrations
mix ecto.migrate

Using Mix tasks:

mix dbml.schemas priv/schema.dbml -o lib/my_app/schema --namespace MyApp --update true
mix dbml.schemas priv/schema.dbml -o lib/my_app/schema --namespace MyApp --overwrite
mix dbml.migrations priv/schema.dbml -o priv/repo/migrations -r MyApp.Repo --update true
mix dbml.migrations priv/schema.dbml -o priv/repo/migrations -r MyApp.Repo --overwrite
mix ecto.migrate

For an existing project (using Elixir)

# Update DBML, then regenerate:
{:ok, tokens} = DBML.parse_file("priv/schema.dbml")

DBML.generate_ecto_schemas(
  tokens,
  "lib/my_app/schema",
  namespace: "MyApp",
  update: true  # overwrite changed, skip unchanged
)

DBML.generate_ecto_schemas(
  tokens,
  "lib/my_app/schema",
  namespace: "MyApp",
  overwrite: true  # overwrite all files
)

DBML.generate_ecto_migrations(
  tokens,
  "priv/repo/migrations",
  "MyApp.Repo",
  update: true  # create new migrations for changed tables
)

# Review and run new migrations
mix ecto.migrate

For visualizing existing schemas (using CLI)

# 1. Generate DBML from your existing Ecto schemas
./dbml file lib/my_app/schema -o priv/schema.dbml --project-name MyApp

# 2. View it online at https://dbml.dbdiagram.io/home
# Copy the contents of priv/schema.dbml and paste it into the editor

Or using Mix task:

mix dbml.file lib/my_app/schema -o priv/schema.dbml --project-name MyApp

For visualizing existing schemas (using Elixir)

# Generate DBML from your existing Ecto schemas
{:ok, path} = DBML.schemas_to_dbml("lib/my_app/schema", "priv/schema.dbml", project_name: "MyApp")

# View it online at https://dbml.dbdiagram.io/home
# Copy the contents of priv/schema.dbml and paste it into the editor

Migrating from Ecto schemas to DBML-first workflow (using CLI)

# 1. Visualize current schemas
./dbml file lib/my_app/schema -o schema.dbml --project-name MyApp

# 2. Use the DBML as the source of truth going forward
# Edit schema.dbml with new changes

# 3. Generate schemas and migrations from DBML
./dbml schemas schema.dbml -o lib/my_app/schema --namespace MyApp --update true
./dbml schemas schema.dbml -o lib/my_app/schema --namespace MyApp --overwrite
./dbml migrations schema.dbml -o priv/repo/migrations -r MyApp.Repo --update true
./dbml migrations schema.dbml -o priv/repo/migrations -r MyApp.Repo --overwrite

# 4. Run migrations
mix ecto.migrate

Or using Mix tasks:

mix dbml.file lib/my_app/schema -o schema.dbml --project-name MyApp
# Edit schema.dbml...
mix dbml.schemas schema.dbml -o lib/my_app/schema --namespace MyApp --update true
mix dbml.schemas schema.dbml -o lib/my_app/schema --namespace MyApp --overwrite
mix dbml.migrations schema.dbml -o priv/repo/migrations -r MyApp.Repo --update true
mix dbml.migrations schema.dbml -o priv/repo/migrations -r MyApp.Repo --overwrite
mix ecto.migrate

Migrating from Ecto schemas to DBML-first workflow (using Elixir)

# 1. Visualize current schemas
{:ok, _path} = DBML.schemas_to_dbml("lib/my_app/schema", "schema.dbml", project_name: "MyApp")

# 2. Use the DBML as the source of truth going forward
# Edit schema.dbml with new changes

# 3. Generate schemas and migrations from DBML
{:ok, tokens} = DBML.parse_file("schema.dbml")
DBML.generate_ecto_schemas(tokens, "lib/my_app/schema", namespace: "MyApp", update: true)
DBML.generate_ecto_migrations(tokens, "priv/repo/migrations", "MyApp.Repo", update: true)

# 4. Run migrations
mix ecto.migrate

Troubleshooting

"File already exists" error

When regenerating schemas or migrations, use update: true or overwrite: true to allow overwrites:

# ❌ This fails if files exist
DBML.generate_ecto_schemas(tokens, "lib/my_app/schema")

# ✅ This overwrites existing files
DBML.generate_ecto_schemas(tokens, "lib/my_app/schema", update: true)
DBML.generate_ecto_schemas(tokens, "lib/my_app/schema", overwrite: true)

Migration ordering with foreign keys

The generator automatically orders migrations using topological sort to ensure referenced tables are created before tables that reference them. This respects table aliases in DBML.

Column/table names with spaces

Names with spaces are converted to snake_case in generated code:

table "user accounts" {
  "full name" varchar
}

Generates:

schema "user accounts" do
  field :full_name, :string
end

Contributing

Contributions are welcome! Please open an issue or pull request on GitHub.


License

Apache-2.0


References