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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 0 additions & 64 deletions .github/workflows/release-audit.yml

This file was deleted.

Binary file modified CHANGELOG.md
Binary file not shown.
27 changes: 13 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SchemaForge

> **Bidirectional ORM schema converter** — convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy, Alembic migrations, JSON Schema, GraphQL SDL, EF Core (C#), and Scala case classes. **11 formats, 110 direction pairs.**
> **Bidirectional ORM schema converter** — convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy, Alembic migrations, JSON Schema, GraphQL SDL, EF Core (C#), and Scala case classes. **11 formats, 100 conversion directions.**

[![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/schemaforge?style=social)](https://github.com/Coding-Dev-Tools/schemaforge/stargazers)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/Coding-Dev-Tools/schemaforge)
Expand Down Expand Up @@ -55,7 +55,7 @@ Requires Python 3.10+.

### `schemaforge convert`

Convert a schema from one format to another. All 11 formats support conversion to and from every other format (110 direction pairs).
Convert a schema from one format to another. Every format converts to and from every other format, except Alembic which is generator-only (a target, not a source) — 100 direction pairs.

```bash
# Format-specific examples
Expand Down Expand Up @@ -115,6 +115,11 @@ Detects added, removed, and modified tables, columns, indexes, and constraints.

**Alembic** is generator-only: you can create migration scripts from any format, but parsing existing migrations back to IR is not yet supported.

### Limitations

- **Foreign keys & relationships** — the shared IR does not yet model foreign-key constraints or ORM relations, so `FOREIGN KEY` / `REFERENCES` clauses, Prisma/TypeORM relation fields, and Django `ForeignKey` fields are dropped during conversion rather than roundtripped. Tables, columns, types, defaults, indexes, unique constraints, and enums are preserved. FK support is on the roadmap.
- **Alembic is generator-only** (see above) — you can generate migrations from any format but not parse them back.

### Format Identifiers for `--from` / `--to`

| CLI identifier | Format |
Expand All @@ -135,8 +140,8 @@ Detects added, removed, and modified tables, columns, indexes, and constraints.

SchemaForge uses a **shared Internal Representation (IR)** — all formats convert to and from this common schema definition. This architecture guarantees:

- **Zero-loss roundtripping**: `sql → prisma → sql` produces the same schema you started with
- **Bidirectional conversion**: every supported format can convert to every other format
- **High-fidelity roundtripping**: `sql → prisma → sql` reproduces tables, columns, types, defaults, indexes, unique constraints, and enums. Foreign-key/relationship constraints are not yet modeled in the IR and are dropped (see [Limitations](#limitations)).
- **Bidirectional conversion**: every format can convert to every other format, except Alembic, which is generator-only (a target, not a source)
- **Extensibility**: adding a new format requires only a parser and a generator — no pairwise converters

```
Expand Down Expand Up @@ -238,8 +243,8 @@ Each fixture demonstrates the same blog schema so you can compare ORM syntax sid

## Features

- **Bidirectional conversion** — all 11 formats convert to and from every other format
- **Zero-loss roundtripping** — `sql → prisma → sql` reproduces the original schema exactly
- **Bidirectional conversion** — every format converts to and from every other format (Alembic is generator-only: a target, not a source)
- **High-fidelity roundtripping** — `sql → prisma → sql` reproduces tables, columns, types, defaults, indexes, and enums (foreign keys are not yet preserved — see [Limitations](#limitations))
- **Custom type mappings** — YAML/JSON config files to override any type mapping with template variables
- **VS Code extension** — live preview, schema diff, and one-click conversion from VS Code
- **Alembic migration generation** — create database migration scripts from any schema format
Expand All @@ -253,7 +258,7 @@ Each fixture demonstrates the same blog schema so you can compare ORM syntax sid
- **Function default preservation** — `CURRENT_TIMESTAMP`, `NOW()`, `gen_random_uuid()` survive roundtrips
- **MySQL support** — ENGINE=InnoDB, AUTO_INCREMENT, DEFAULT CHARSET, COMMENT table options
- **Inline ENUM** — `ENUM('small', 'medium', 'large')` column types parsed and roundtripped
- **Relation preservation** — indexes, unique constraints maintained across all conversions
- **Index & constraint preservation** — indexes and unique constraints maintained across all conversions (foreign-key/relationship constraints are not yet modeled — see [Limitations](#limitations))
- **Custom type handling** — dialect-specific types (JSONB, etc.) pass through via CUSTOM type

## MCP Server
Expand Down Expand Up @@ -442,10 +447,4 @@ MIT — see [LICENSE](LICENSE)

---

<sub>Part of [Revenue Holdings](https://coding-dev-tools.github.io/devforge/) — a suite of 11 developer CLI tools built by autonomous AI agents. Also check out the [SchemaForge VS Code extension](https://github.com/Coding-Dev-Tools/vscode-schemaforge), [ConfigDrift](https://github.com/Coding-Dev-Tools/configdrift) (config drift detection), [DataMorph](https://github.com/Coding-Dev-Tools/datamorph) (data format conversion), [DeadCode](https://github.com/Coding-Dev-Tools/deadcode) (dead code cleanup), [DeployDiff](https://github.com/Coding-Dev-Tools/deploydiff) (infrastructure diffs), [Envault](https://github.com/Coding-Dev-Tools/envault) (env sync), [APIAuth](https://github.com/Coding-Dev-Tools/apiauth) (API key management), [APIGhost](https://github.com/Coding-Dev-Tools/apighost) (mock API server), [json2sql](https://github.com/Coding-Dev-Tools/json2sql) (JSON → SQL), [API Contract Guardian](https://github.com/Coding-Dev-Tools/api-contract-guardian) (breaking change detection), and [click-to-mcp](https://github.com/Coding-Dev-Tools/click-to-mcp) (CLI → MCP server).</sub>

## Test

```bash
npm test # runs: node --test tests/
```
<sub>Part of [Revenue Holdings](https://coding-dev-tools.github.io/devforge/) — a s
9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ version = "1.7.0"
description = "Bidirectional ORM schema converter — convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy, Alembic, JSON Schema, GraphQL SDL, EF Core (C#), and Scala case classes with zero-loss roundtripping"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license = {file = "LICENSE"}
authors = [{name = "Revenue Holdings"}]
keywords = ["schema", "orm", "prisma", "drizzle", "typeorm", "django", "sql", "converter", "migration"]
keywords = ["schema", "orm", "prisma", "drizzle", "typeorm", "django", "sql", "converter", "migration", "alembic", "graphql", "json-schema", "ef-core", "scala", "code-generation"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Database",
"Topic :: Software Development :: Code Generators",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand Down Expand Up @@ -48,13 +49,15 @@ schemaforge = "schemaforge.cli:main"
Homepage = "https://github.com/Coding-Dev-Tools/schemaforge"
Repository = "https://github.com/Coding-Dev-Tools/schemaforge"
"Issue Tracker" = "https://github.com/Coding-Dev-Tools/schemaforge/issues"
Documentation = "https://github.com/Coding-Dev-Tools/schemaforge#readme"
Changelog = "https://github.com/Coding-Dev-Tools/schemaforge/blob/main/CHANGELOG.md"

[tool.setuptools.packages.find]
where = ["src"]


[tool.setuptools.package-data]
"*" = ["py.typed"]

[tool.pytest.ini_options]
testpaths = ["tests"]

Expand Down
77 changes: 65 additions & 12 deletions src/schemaforge/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import click
import json
import sys
from pathlib import Path

Expand Down Expand Up @@ -35,28 +36,31 @@ def main() -> None:
"""SchemaForge — bidirectional ORM schema converter.

Convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy models,
Alembic migrations, JSON Schema, and GraphQL SDL with zero-loss roundtripping.
Alembic migrations, JSON Schema, and GraphQL SDL with high-fidelity
roundtripping (foreign-key/relationship constraints are not yet preserved).
"""


@main.command()
@click.argument(
"input_arg",
required=False,
type=click.Path(exists=True, readable=True),
)
@click.option(
"--from",
"from_fmt",
required=True,
type=click.Choice(_FORMATS),
help="Source format",
)
@click.option(
"--to", "to_fmt", required=True, type=click.Choice(_FORMATS), help="Target format"
)
@click.option("--to", "to_fmt", required=True, type=click.Choice(_FORMATS), help="Target format")
@click.option(
"--input",
"-i",
"input_path",
required=True,
"input_opt",
type=click.Path(exists=True, readable=True),
help="Input file path",
help="Input file path (alternative to the positional INPUT_ARG)",
)
@click.option(
"--output",
Expand All @@ -72,13 +76,27 @@ def main() -> None:
help="Custom type mapping config file (.yaml or .json)",
)
def convert(
input_arg: str | None,
from_fmt: str,
to_fmt: str,
input_path: str,
input_opt: str | None,
output_path: str | None,
type_map_path: str | None,
) -> None:
"""Convert schema between formats."""
"""Convert schema between formats.

The input file may be given either as a positional argument
(``schemaforge convert schema.sql --from sql --to prisma``) or via
``--input``/``-i`` — the two are interchangeable.
"""
input_path = input_arg or input_opt
if not input_path:
click.echo(
"Error: no input file given. Pass a path argument or use --input/-i.",
err=True,
)
sys.exit(1)

# Load custom type mapping if specified
type_config: TypeConfig | None = None
if type_map_path:
Expand Down Expand Up @@ -157,9 +175,7 @@ def check(directory: str, canonical: str, type_map_path: str | None) -> None:
consistency across format representations.
"""
try:
result = check_directory(
directory, canonical=canonical, type_map_path=type_map_path
)
result = check_directory(directory, canonical=canonical, type_map_path=type_map_path)
click.echo(result)
if "FAIL" in result and "PASS" not in result:
sys.exit(1)
Expand All @@ -168,6 +184,43 @@ def check(directory: str, canonical: str, type_map_path: str | None) -> None:
sys.exit(1)


@main.command()
@click.argument("input_path", type=click.Path(exists=True, readable=True))
@click.option("--verbose", "-v", is_flag=True, help="Show detailed detection info")
def detect(input_path: str, verbose: bool) -> None:
"""Detect the schema format of a file from its extension.

Prints the bare format identifier (e.g. ``prisma``) on success, or
``unknown`` if the extension is not recognized. The plain output is meant
to be consumed directly (the VS Code extension reads it as the source
format for a follow-up convert).
"""
fmt = detect_format(input_path)
if verbose:
ext = Path(input_path).suffix.lower() or "(none)"
click.echo(f"file: {input_path}")
click.echo(f"extension: {ext}")
click.echo(f"format: {fmt if fmt else 'unknown'}")
click.echo("method: file extension")
else:
click.echo(fmt if fmt else "unknown")


@main.command()
@click.option("--json", "as_json", is_flag=True, help="Output the format list as a JSON array")
def formats(as_json: bool) -> None:
"""List all supported schema formats.

With ``--json`` prints a JSON array of format identifiers (consumed by the
VS Code extension); otherwise prints one format identifier per line.
"""
if as_json:
click.echo(json.dumps(_FORMATS))
else:
for fmt in _FORMATS:
click.echo(fmt)


# Register the MCP server subcommand
main.add_command(mcp_command)

Expand Down
26 changes: 25 additions & 1 deletion src/schemaforge/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import annotations

import click
import os
from pathlib import Path
from typing import Any

Expand All @@ -23,6 +24,26 @@
FastMCP = None # type: ignore


def _confined_directory(directory: str) -> Path:
"""Resolve *directory* and confirm it stays within the allowed root.

The ``check`` tool iterates and reads files under the given directory. To
keep an AI agent (or, in SSE mode, a remote caller) from reading arbitrary
locations on the host, requests are confined to a root — the
``SCHEMAFORGE_MCP_ROOT`` environment variable if set, otherwise the current
working directory the server was launched in. Escaping the root raises
``PermissionError``.
"""
root = Path(os.environ.get("SCHEMAFORGE_MCP_ROOT", Path.cwd())).resolve()
target = Path(directory).resolve()
if target != root and not target.is_relative_to(root):
raise PermissionError(
f"Directory '{directory}' is outside the allowed root '{root}'. "
f"Set SCHEMAFORGE_MCP_ROOT to permit a different base directory."
)
return target


# All supported formats
_FORMATS = [
"sql",
Expand Down Expand Up @@ -156,10 +177,13 @@ def check_tool(
type_map_path: Optional path to a YAML/JSON type mapping config file.
"""
try:
safe_dir = _confined_directory(directory)
result = check_directory(
directory, canonical=canonical, type_map_path=type_map_path
str(safe_dir), canonical=canonical, type_map_path=type_map_path
)
return result
except PermissionError as e:
return f"Error: {e}"
except NotADirectoryError as e:
return f"Error: {e}"
except Exception as e:
Expand Down
Loading
Loading