Metadata-Version: 2.4
Name: al2dbml
Version: 0.3.3
Summary: Convert compiled Microsoft Dynamics 365 Business Central AL packages (.app) into DBML schemas.
Author-email: Mykola Kharchenko <mykola.kharchenko@outlook.com>
License: MIT
Project-URL: Homepage, https://github.com/kharmyko/al2dbml
Project-URL: Issues, https://github.com/kharmyko/al2dbml/issues
Keywords: dbml,dbdiagram,dbdocs,erd,database-diagram,schema,business-central,dynamics-365,microsoft-dynamics,bc,al,al-language,symbol-reference
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.1
Requires-Dist: pydbml>=1.2.0
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Dynamic: license-file

# al2dbml

`al2dbml` is a small Python CLI that converts a compiled Microsoft Dynamics 365 Business Central AL package (`.app`) into a [DBML](https://dbml.dbdiagram.io/) schema you can paste straight into [dbdiagram.io](https://dbdiagram.io) or [dbdocs.io](https://dbdocs.io). The pipeline reads `SymbolReference.json` from the `.app` archive (tolerating AL's 40-byte header), normalises tables, extensions, enums, and `TableRelation`s, and emits one valid DBML document with `Table`, `Ref`, `Enum`, and `TableGroup` sections.

## What's new in 0.3.3

- **Enum items now carry their AL ordinal as a note** — every enum value is rendered as `"Name" [note: '<n>']` so you can read the integer-to-name mapping directly off the diagram. BC stores enum values as integers in SQL, so this is what you need when you see `Type = 2` in a row dump and want to know which entry it was without flipping back to the AL source. `Approval Action` from Base Application, for example, deliberately starts at ordinal 1; you can see that gap in the diagram now.

## What's new in 0.3.2

- **`--stats` is now fast** — on Microsoft's Base Application it went from ~3–5 minutes to ~4 seconds. Two stacked fixes: when `--stats` is the only output requested, we skip the DBML render entirely (it's O(n²)); and we bypass `pydbml.Database.add_reference`'s redundant duplicate-check on every ref we add (we already dedupe upstream by id pair). Generating to a file (`-o`) still pays the pydbml render cost — separate slice on the roadmap.

## What's new in 0.3.1

- **`al2dbml-validate FILE`** — second console script that parses a DBML file through pydbml and reports syntax errors with line/column. Exit code 0 on success, non-zero on parse error. For the authoritative check matching dbdiagram.io exactly, install [`@dbml/cli`](https://www.npmjs.com/package/@dbml/cli) (`npm i -g @dbml/cli`) and run `dbml2sql FILE --postgres`.
- **Empty enum values fixed** — AL sometimes encodes a default/blank enum slot as `""`, which broke DBML's parser. Now silently substituted with `" "` (single space) so the slot still appears.
- **Self-referential refs dropped** — some BC tables declare a `TableRelation` from a field back to itself (e.g. *Production Order.No.* → *Production Order.No.*). Those are now skipped instead of being emitted as a meaningless `Ref { T.f > T.f }`.

## What's new in 0.3.0

- **`--include` / `--exclude` table filters** — carve out a slice of a large package by name pattern (essential for Microsoft's Base Application, which has 1,500+ tables).
- **Namespace-aware grouping** — `TableGroup`s now default to the last segment of each table's AL namespace (so `Microsoft.Finance.GeneralLedger` -> `GeneralLedger`); switch back with `--group-by word` or off with `--group-by none`.
- **`--stats` flag + empty-output warning** — quick post-run sanity check; codeunit-only extensions (like *Sales and Inventory Forecast*) now tell you "0 tables and 0 enums" up front instead of silently producing an empty file.
- **DBML provenance header** — every output begins with `// Generated by al2dbml <version> from <Name> <Version> by <Publisher>` and an `// AppId:` line, so you can tell a stray `.dbml` apart from its sibling six months later.

## Install

Python 3.10+ is required. The runtime depends only on [`click`](https://click.palletsprojects.com/) and [`pydbml`](https://github.com/Vanderhoof/PyDBML).

### Recommended: `uv tool install`

[`uv`](https://docs.astral.sh/uv/) installs CLI tools into isolated environments and puts the entry point on your `PATH`, so `al2dbml` is available globally without touching your system Python.

```bash
uv tool install al2dbml
```

If you don't already have `uv`:

```bash
# Fedora / RHEL / CentOS
sudo dnf install uv

# macOS (Homebrew)
brew install uv

# Anywhere (standalone installer)
curl -LsSf https://astral.sh/uv/install.sh | sh
```

Upgrade later with `uv tool upgrade al2dbml`, uninstall with `uv tool uninstall al2dbml`.

### Alternative: pipx

```bash
pipx install al2dbml
```

### Alternative: plain pip

Works inside an activated virtualenv. On modern distros that mark system Python as externally-managed (PEP 668), prefer `uv tool` or `pipx` instead.

```bash
pip install al2dbml
```

### Verify

```bash
al2dbml --version
al2dbml --help
```

## Quickstart

```bash
al2dbml MyApp.app -o schema.dbml
```

Drop `schema.dbml` into <https://dbdiagram.io>. Without `-o`, the DBML is streamed to stdout so you can pipe it elsewhere.

```bash
al2dbml MyApp.app | less
```

## Grouping

By default tables are bucketed into `TableGroup`s by the last segment of their AL namespace (so `Microsoft.Finance.GeneralLedger` -> group `GeneralLedger`). Tables that have no namespace tag fall back to the first whitespace-separated word in their name (so `Sales Header` + `Sales Line` -> group `Sales`). Buckets smaller than two tables are dropped so single-table groups don't clutter the diagram.

Override the source with `--group-by`:

```bash
al2dbml MyApp.app --group-by namespace   # default
al2dbml MyApp.app --group-by word        # legacy first-word grouping
al2dbml MyApp.app --group-by none        # no auto groups (only explicit --group rules apply)
```

```bash
# Auto grouping (default)
al2dbml MyApp.app -o schema.dbml

# Explicit rules; the value is NAME=PATTERN[,PATTERN...] and -g is repeatable
al2dbml MyApp.app -g "Documents=Sales*,Purch*" -g "Master=Customer,Vendor,Item"

# Disable grouping entirely
al2dbml MyApp.app --no-groups

# Keep singleton groups too
al2dbml MyApp.app --min-group-size 1
```

`--no-auto-groups` switches off the first-word fallback so only your explicit `-g` rules apply.

## TableExtensions

Extensions are merged into their target tables by default. Use `--no-merge-extensions` to emit them as separate `<Target> (Extension)` tables instead.

## Public Python API

```python
from al2dbml import Generator, generate, GroupingConfig

# One-shot helper
dbml = generate("MyApp.app", output_path="schema.dbml")

# Or step-by-step for custom grouping
gen = Generator.from_app(
    "MyApp.app",
    grouping=GroupingConfig(rules={"Documents": ["Sales*", "Purch*"]}),
)
print(gen.dbml())
```

## Limitations

- FlowFields are treated as regular fields — the underlying CalcFormula is not interpreted.
- Obsolete fields are emitted alongside active ones; no filtering by `ObsoleteState`.
- Multi-field primary keys are represented as multiple `[pk]` flags rather than a composite index, matching DBML's single-PK convention.
- Multi-column secondary keys are not yet emitted as DBML indexes; only single-column secondary keys are surfaced (as `[unique]` on the column).
- Cross-package references (table relations that point to a table outside the current `.app`) are preserved as notes on the source column, since the target table is not present in the diagram.
- `IF (...) ... ELSE IF (...) ... ELSE ...` conditional `TableRelation` expressions are parsed into one DBML `Ref` per resolved branch, with each branch's condition recorded in the source column's note. Branches whose target table is missing from the current `.app` degrade to notes only.
- Render time scales quadratically with the table count inside the underlying `pydbml` library. Small/medium packages (up to a few hundred tables) finish in under a second. Microsoft's full Base Application (~1,500 tables) currently takes several minutes to render, even though parsing itself is fast. A custom DBML emitter is on the roadmap to remove this cliff.

## Development

```bash
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest -q
.venv/bin/ruff check .
```
