ormbridge 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -597
- package/dist/cliRunner.js +14 -4
- package/dist/cliRunner.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/formats.d.ts +5 -0
- package/dist/formats.js +6 -1
- package/dist/formats.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,662 +1,154 @@
|
|
|
1
1
|
# ormbridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
- Author: [Richard McQuiston](mailto:richard.mcquiston@gmail.com)
|
|
4
|
+
- Website: https://richardmcquiston.com/
|
|
5
|
+
|
|
6
|
+
## Table of Contents
|
|
7
|
+
|
|
8
|
+
| [Overview](#overview) | [Getting Started](#getting-started) | [More Information](#more-information) | [Buy Me a Coffee](#buy-me-a-coffee) | [License](#license) | [Copyright](#copyright) |
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
ormbridge converts data models from one Object-Relational Mapper (ORM) to another. You define a model once and share it across your whole stack. Use it from the command line or from code.
|
|
13
|
+
|
|
14
|
+
It reads source files with [tree-sitter](https://tree-sitter.github.io/), so you do not need Python, PHP, Go, or a database. The default `preserve` mode keeps your table and column names, so a new tool can sit on top of the database you already have. When something cannot be converted, ormbridge prints a warning that names the model and field.
|
|
4
15
|
|
|
5
16
|
| Format | Read | Write | Notes |
|
|
6
17
|
| --------------------------- | :--: | :---: | --------------------------------------------------------------------- |
|
|
7
18
|
| Django models | ✔ | ✔ | `models.py` files or `models/` packages |
|
|
8
|
-
| Prisma schema | ✔ | ✔ | All six providers; Prisma 7 header
|
|
19
|
+
| Prisma schema | ✔ | ✔ | All six providers; Prisma 7 header with `--prisma-version 7` |
|
|
9
20
|
| TypeORM entities | ✔ | ✔ | Decorator-based entity classes |
|
|
10
|
-
| Doctrine ORM entities (PHP) | ✔ | ✔ | PHP 8 attributes;
|
|
21
|
+
| Doctrine ORM entities (PHP) | ✔ | ✔ | PHP 8 attributes; one file per entity |
|
|
11
22
|
| Laravel (PHP) | ✔ | ✔ | Reads migrations plus Eloquent models; writes both |
|
|
12
|
-
| GORM (Go) | ✔ | ✔ | Structs with `gorm` tags;
|
|
13
|
-
| Drizzle ORM (TypeScript) | ✔ | ✔ | `pgTable
|
|
23
|
+
| GORM (Go) | ✔ | ✔ | Structs with `gorm` tags; one file per model |
|
|
24
|
+
| Drizzle ORM (TypeScript) | ✔ | ✔ | `pgTable`, `mysqlTable`, `sqliteTable` plus `relations()` |
|
|
14
25
|
| JSON Schema / OpenAPI | ✔ | ✔ | Draft 2020-12, 2019-09 and 7, and OpenAPI `components.schemas` (JSON) |
|
|
15
|
-
| SQL DDL | ✔ |
|
|
16
|
-
| SQLAlchemy / SQLModel | ✔ |
|
|
26
|
+
| SQL DDL | ✔ | ✔ | `CREATE TABLE` and more for PostgreSQL, MySQL, SQLite, SQL Server |
|
|
27
|
+
| SQLAlchemy / SQLModel | ✔ | ✔ | SQLAlchemy 2.0 (declarative, `Column()`, `Table()`) and SQLModel |
|
|
17
28
|
| TypeScript interfaces | | ✔ | Plain interfaces and enums for front ends |
|
|
18
29
|
| Graphene (graphene-django) | | ✔ | GraphQL types, queries and mutations that pair with Django |
|
|
19
|
-
| Zod schemas | | ✔ | Zod 4 schemas and inferred types
|
|
20
|
-
| SQL DDL | | ✔ | One `.sql` script for PostgreSQL, MySQL, SQLite or SQL Server |
|
|
21
|
-
| SQLAlchemy / SQLModel | | ✔ | SQLAlchemy 2.0 declarative models; `--style sqlmodel` for SQLModel |
|
|
30
|
+
| Zod schemas | | ✔ | Zod 4 schemas and inferred types |
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
Each format has a [guide](./docs/formats/README.md) and a runnable [example project](./examples/README.md).
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
- **Safe for existing databases.** The default `preserve` mode keeps your existing table and column names, so a new tool can sit on top of the database you already have.
|
|
27
|
-
- **Honest output.** Anything that cannot be represented produces a warning naming the model and field.
|
|
34
|
+
[Back to Top](#table-of-contents)
|
|
28
35
|
|
|
29
|
-
##
|
|
36
|
+
## Getting Started
|
|
37
|
+
|
|
38
|
+
### Prerequisites
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
- Node.js 20 or newer.
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
### Installation
|
|
34
43
|
|
|
35
44
|
```bash
|
|
36
|
-
npm install --global ormbridge
|
|
45
|
+
npm install --global ormbridge
|
|
37
46
|
```
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
You can also run it without installing: `npx ormbridge convert ...`.
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
### Usage
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
Run `ormbridge convert` with an input, an output, and optionally the formats:
|
|
44
53
|
|
|
45
54
|
```bash
|
|
46
|
-
# Django -> Prisma
|
|
47
55
|
ormbridge convert -i ./shop/models.py -o ./prisma/schema.prisma
|
|
56
|
+
```
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
ormbridge convert -i ./backend -o ./prisma/schema.prisma
|
|
51
|
-
|
|
52
|
-
# Prisma -> Django
|
|
53
|
-
ormbridge convert -i ./prisma/schema.prisma -o ./shop/models.py --app-label shop
|
|
54
|
-
|
|
55
|
-
# TypeORM -> Prisma (a directory needs --from; every .ts file in it is read, entities are picked out)
|
|
56
|
-
ormbridge convert -i ./src/entities --from typeorm -o ./prisma/schema.prisma
|
|
57
|
-
|
|
58
|
-
# Doctrine (PHP 8 attributes) -> Prisma (every .php file is read; vendor/, var/ and *Test.php are skipped)
|
|
59
|
-
ormbridge convert -i ./src/Entity --from doctrine -o ./prisma/schema.prisma
|
|
60
|
-
|
|
61
|
-
# Laravel project root -> Prisma (reads database/migrations and app/; vendor/, storage/, tests/ and bootstrap/cache are skipped)
|
|
62
|
-
ormbridge convert -i ./my-laravel-app --from laravel -o ./prisma/schema.prisma
|
|
58
|
+
Django (`.py`), Prisma (`.prisma`), and SQL (`.sql`) files are detected from the file extension. For every other format, add `--from` or `--to`. List all formats with `ormbridge formats`.
|
|
63
59
|
|
|
64
|
-
|
|
65
|
-
ormbridge convert -i ./internal/models --from gorm -o ./prisma/schema.prisma
|
|
60
|
+
Useful options:
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
- `--naming preserve|normalize`: keep existing names (default), or write singular snake_case names with UUID keys.
|
|
63
|
+
- `--provider <name>`: the database to target, such as `postgresql` or `mysql`.
|
|
64
|
+
- `--dry-run`: show what would be written, and write nothing.
|
|
65
|
+
- `--check`: exit with code 3 if an output file is out of date. Use it in Continuous Integration (CI).
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
ormbridge convert -i ./schemas --from json-schema -o ./blog/models.py
|
|
72
|
-
ormbridge convert -i ./openapi.json --from json-schema --to prisma -o ./prisma/schema.prisma
|
|
67
|
+
You can keep your settings in an `ormbridge.config.json` file. See the [command line reference](./docs/CLI.md) for every option, the config file, and exit codes.
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
ormbridge convert -i ./schema.sql -o ./prisma/schema.prisma
|
|
76
|
-
ormbridge convert -i ./db/migrations --from sql --to django -o ./blog/models.py
|
|
69
|
+
### Examples
|
|
77
70
|
|
|
78
|
-
|
|
79
|
-
ormbridge convert -i ./app/models --from sqlalchemy -o ./prisma/schema.prisma
|
|
80
|
-
# Prisma -> SQLAlchemy 2.0 models (".py" means Django, so pass --to sqlalchemy; --style sqlmodel writes SQLModel classes instead)
|
|
81
|
-
ormbridge convert -i ./prisma/schema.prisma --to sqlalchemy -o ./app/models.py
|
|
82
|
-
ormbridge convert -i ./prisma/schema.prisma --to sqlalchemy --style sqlmodel -o ./app/models.py
|
|
83
|
-
```
|
|
71
|
+
Prisma to Django:
|
|
84
72
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
| Flag | Default | Description |
|
|
88
|
-
| ------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
89
|
-
| `-i, --input <paths...>` | required (or config) | Files or directories to read. Several files are merged into one schema, so abstract base classes can live in another file. |
|
|
90
|
-
| `-o, --output <path>` | stdout | File to write. Parent directories are created. |
|
|
91
|
-
| `-f, --from <format>` | inferred | `django`, `prisma`, `typeorm`, `drizzle`, `doctrine`, `laravel`, `gorm`, `json-schema`, `sql` or `sqlalchemy` |
|
|
92
|
-
| `-t, --to <format>` | inferred | `django`, `prisma`, `typeorm`, `drizzle`, `doctrine`, `laravel`, `gorm`, `json-schema`, `sql` or `sqlalchemy` (`drizzle`, `doctrine`, `laravel` and `gorm` write a directory), `typescript`, `zod` or `graphene` (output only) |
|
|
93
|
-
| `--naming <mode>` | `preserve` | `preserve` or `normalize` (see below) |
|
|
94
|
-
| `--provider <name>` | `postgresql` | Database: `postgresql`, `mysql`, `sqlite`, `sqlserver`, `mongodb`, `cockroachdb`. Controls Prisma native types such as `@db.VarChar`, the Drizzle and GORM dialect and the SQL dialect of `--to sql` (`postgresql`, `mysql`, `sqlite` or `sqlserver`). |
|
|
95
|
-
| `--no-header` | off | Omit the Prisma `generator` / `datasource` blocks (useful when pasting models into an existing schema). |
|
|
96
|
-
| `--app-label <name>` | directory name | Django app label used for default table names (`<app>_<model>`). |
|
|
97
|
-
| `--auto-field <type>` | `int` | Key type for Django models without an explicit primary key: `int` or `bigint`. |
|
|
98
|
-
| `--namespace <name>` | `App\Entity`, `App\Models` | PHP namespace of the Doctrine entities or Laravel models (`--to doctrine` / `--to laravel` only). Enums go in a sibling namespace. |
|
|
99
|
-
| `--go-package <name>` | `models` | Go package name of the GORM models, also the output sub-directory (`--to gorm` only). Must be a valid Go identifier. |
|
|
100
|
-
| `--prisma-version <6\|7>` | `6` | Prisma major version of the header (`--to prisma` only). `7` writes the `prisma-client` generator (with an `output`) and no datasource `url`. |
|
|
101
|
-
| `--style <name>` | `sqlalchemy` | Flavour of the SQLAlchemy output (`--to sqlalchemy` only): `sqlalchemy` (declarative `Mapped[...]` models) or `sqlmodel` (`SQLModel, table=True` classes). Anything else fails with the valid values. |
|
|
102
|
-
| `--dry-run` | off | Run the whole conversion and report what would be written, without touching the filesystem. |
|
|
103
|
-
| `--check` | off | Exit with code 3 if an output file is missing or differs from the conversion. Writes nothing. Needs an output path. |
|
|
104
|
-
| `--config <path>` | searched | Read settings from this JSON config file instead of searching for one. |
|
|
105
|
-
| `--no-config` | off | Ignore any config file. |
|
|
106
|
-
|
|
107
|
-
### Dry run and check
|
|
108
|
-
|
|
109
|
-
`--dry-run` converts everything exactly as a real run would and reports each file it would create or update (line count, size, and for an existing file a short diff summary such as `+3 -1 lines (first difference at line 12)`), plus all warnings. Nothing is written.
|
|
110
|
-
|
|
111
|
-
`--check` is for CI. It compares the conversion with the existing output and exits `0` when every file is up to date. If a file is missing or differs, it exits `3` and names the file:
|
|
112
|
-
|
|
113
|
-
```text
|
|
114
|
-
error: prisma/schema.prisma is out of date: +3 -1 lines (first difference at line 12).
|
|
115
|
-
Generated output is stale. Run "ormbridge convert" without --check to regenerate it.
|
|
73
|
+
```bash
|
|
74
|
+
ormbridge convert -i ./prisma/schema.prisma -o ./shop/models.py
|
|
116
75
|
```
|
|
117
76
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
### Config file
|
|
121
|
-
|
|
122
|
-
Instead of repeating flags, put them in `ormbridge.config.json` (or `.ormbridgerc.json`). ormbridge looks in the current directory and each parent directory, and stops after the first directory that contains a `package.json` (or at the filesystem root). Use `--config <path>` to pick a file explicitly, or `--no-config` to ignore it. The file is plain JSON; no code is executed. Relative paths inside it resolve against the config file's directory, not the working directory.
|
|
123
|
-
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"naming": "normalize",
|
|
127
|
-
"provider": "postgresql",
|
|
128
|
-
"conversions": [
|
|
129
|
-
{
|
|
130
|
-
"name": "prisma",
|
|
131
|
-
"input": "backend",
|
|
132
|
-
"output": "prisma/schema.prisma"
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
"name": "types",
|
|
136
|
-
"input": "backend",
|
|
137
|
-
"to": "typescript",
|
|
138
|
-
"output": "web/src/models.ts"
|
|
139
|
-
}
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
```
|
|
77
|
+
A TypeORM project to Prisma (a folder needs `--from`):
|
|
143
78
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
| `input` | string or string list | `-i, --input` |
|
|
147
|
-
| `output` | string | `-o, --output` |
|
|
148
|
-
| `from` | string | `-f, --from` |
|
|
149
|
-
| `to` | string | `-t, --to` |
|
|
150
|
-
| `naming` | string | `--naming` (`preserve` or `normalize`) |
|
|
151
|
-
| `provider` | string | `--provider` |
|
|
152
|
-
| `header` | boolean | `--no-header` when `false` |
|
|
153
|
-
| `appLabel` | string | `--app-label` |
|
|
154
|
-
| `autoField` | string | `--auto-field` (`int` or `bigint`) |
|
|
155
|
-
| `namespace` | string | `--namespace` (Doctrine or Laravel output) |
|
|
156
|
-
| `goPackage` | string | `--go-package` (GORM output) |
|
|
157
|
-
| `prismaVersion` | number | `--prisma-version` (`6` or `7`; Prisma output) |
|
|
158
|
-
| `style` | string | `--style` (`sqlalchemy` or `sqlmodel`; SQLAlchemy output) |
|
|
159
|
-
| `conversions` | list of objects | Several named conversions; each takes the keys above plus `name` |
|
|
160
|
-
|
|
161
|
-
Top-level keys are defaults for every entry in `conversions`. Flags override config values. With no `-i`, `ormbridge convert` runs every conversion in the list (and `-o` is rejected, since the conversions write to different paths); with `-i`, it runs a single conversion from the top-level settings. Unknown keys and bad values fail with a message naming the key, such as `"conversions[1].naming" must be "preserve" or "normalize" (got "weird")`.
|
|
162
|
-
|
|
163
|
-
To fail CI when generated files are stale:
|
|
164
|
-
|
|
165
|
-
```yaml
|
|
166
|
-
# .github/workflows/ci.yml
|
|
167
|
-
- run: npx ormbridge convert --check
|
|
79
|
+
```bash
|
|
80
|
+
ormbridge convert --from typeorm -i ./src/entities -o ./prisma/schema.prisma
|
|
168
81
|
```
|
|
169
82
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
| Code | Meaning |
|
|
173
|
-
| ---- | ---------------------------------------------------------------------------------------------------- |
|
|
174
|
-
| `0` | Success (with `--check`: every output is up to date). |
|
|
175
|
-
| `1` | A conversion failed: unreadable input, parse error, unwritable output. Other conversions still run. |
|
|
176
|
-
| `2` | Usage or config error: bad flag or value, unknown format, invalid config file. Nothing is converted. |
|
|
177
|
-
| `3` | `--check` found an output file that is missing or stale. |
|
|
178
|
-
|
|
179
|
-
### Multi-file output
|
|
180
|
-
|
|
181
|
-
Most formats produce one text, written to the `-o` file. A format can instead return several files (a map of relative path to text; the Doctrine, Laravel, GORM and Drizzle formats do this). Then `-o` must be a directory (for example `ormbridge convert -i ./models.py --to doctrine -o ./`, or `--to laravel -o ./` to write `app/` and `database/migrations/` into a Laravel project): every file is written beneath it, creating folders as needed, and `--dry-run` and `--check` cover each file. If `-o` is an existing file or looks like a file path (it has an extension), ormbridge fails with a message listing the files and asking for a directory. See "Emitter output" under [Format registry](#format-registry) for the adapter side.
|
|
182
|
-
|
|
183
|
-
[Back to Table of Contents](#table-of-contents)
|
|
184
|
-
|
|
185
|
-
### Naming modes
|
|
186
|
-
|
|
187
|
-
**`preserve`** (default) keeps the existing database working unchanged: Django table names (`blog_post`) are kept via `@@map`, columns keep their names, and each Django many-to-many field becomes an explicit join model that matches the table Django already created.
|
|
188
|
-
|
|
189
|
-
**`normalize`** produces a fresh-schema style: singular snake_case table names (`post`), UUID primary keys in place of auto-increment ids, camelCase Prisma fields mapped to snake_case columns, `created_at` / `updated_at` added to models that lack them, and implicit many-to-many relations. Laravel and GORM output apply their own frameworks' conventions instead (plural snake_case tables, keys keep their type); see their sections below.
|
|
190
|
-
|
|
191
|
-
## What is converted
|
|
192
|
-
|
|
193
|
-
| Django | Prisma | TypeORM | Doctrine |
|
|
194
|
-
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
-
| `CharField`, `SlugField`, `EmailField`, `URLField` | `String @db.VarChar(n)` | `@Column({ type: 'varchar', length: n })` | `type: 'string'`, `length: n` |
|
|
196
|
-
| `TextField` | `String @db.Text` | `@Column({ type: 'text' })` | `type: 'text'` |
|
|
197
|
-
| `Integer*`, `PositiveInteger*` / `BigInteger*` / `AutoField` / `BigAutoField` | `Int` / `BigInt` / `@default(autoincrement())` | `int` / `bigint` (`string`), `@PrimaryGeneratedColumn('increment')` | `integer` / `bigint` (`string`), `#[ORM\GeneratedValue(strategy: 'AUTO')]` |
|
|
198
|
-
| `FloatField`, `DecimalField` | `Float`, `Decimal @db.Decimal(p, s)` | `float`, `decimal` with `precision` / `scale` (`string`) | `float`, `decimal` with `precision` / `scale` (`string`) |
|
|
199
|
-
| `BooleanField`, `UUIDField`, `JSONField`, `BinaryField` | `Boolean`, `String @db.Uuid`, `Json`, `Bytes` | `boolean`, `uuid`, `jsonb`, `bytea` (types follow `--provider`) | `boolean`, `guid`, `json` (`jsonb` on PostgreSQL), `blob` |
|
|
200
|
-
| `DateTimeField`, `DateField`, `TimeField` | `DateTime` (with `@db.Date` / `@db.Time`) | `timestamptz` / `date` / `time` | `DateTimeImmutable` (`datetimetz_immutable` on PostgreSQL, `date_immutable`, `time_immutable`) |
|
|
201
|
-
| `ArrayField(base_field)` (nested arrays too) | scalar list `String[]` (PostgreSQL, CockroachDB, MongoDB) | `array: true` on PostgreSQL, `json` elsewhere | `json` (PHP `array`) plus a warning |
|
|
202
|
-
| `HStoreField` | `Json` | `hstore` on PostgreSQL, `json` elsewhere | `json` plus a warning |
|
|
203
|
-
| `IntegerRangeField`, `BigIntegerRangeField`, `DecimalRangeField`, `DateRangeField`, `DateTimeRangeField` | `Unsupported("int4range")` and friends on PostgreSQL, `String` elsewhere | `int4range`, `int8range`, `numrange`, `daterange`, `tstzrange` on PostgreSQL | `string` plus a warning |
|
|
204
|
-
| `GenericIPAddressField`, `IPAddressField` | `String @db.Inet` (PostgreSQL) | `inet` on PostgreSQL, `varchar(45)` elsewhere | `string` (length 45) |
|
|
205
|
-
| `DurationField` | `BigInt` (no interval type) | `interval` on PostgreSQL, `bigint` elsewhere | `dateinterval` plus a warning |
|
|
206
|
-
| `FileField`, `ImageField`, `FilePathField`, `SmallIntegerField`, `PositiveSmallIntegerField`, `SmallAutoField` | `String`, `Int` as above | `varchar(100)`, `int` as above | `string(100)`, `integer` as above |
|
|
207
|
-
| `GeneratedField` (`expression`, `output_field`, `db_persist`) | regular column plus a warning | regular column plus a warning | regular column plus a warning |
|
|
208
|
-
| `db_default` (`Value(...)`, `Now()`, literals) | `@default(...)` | `default: ...` | `options: ['default' => ...]` |
|
|
209
|
-
| `auto_now_add` / `auto_now` | `@default(now())` / `@updatedAt` | `@CreateDateColumn` / `@UpdateDateColumn` | constructor default plus `#[ORM\PrePersist]` / `#[ORM\PreUpdate]` callbacks |
|
|
210
|
-
| `default=uuid.uuid4`, `timezone.now`, literals | `@default(uuid())`, `@default(now())`, literals | `@PrimaryGeneratedColumn('uuid')`, `default: () => 'CURRENT_TIMESTAMP'`, literals | constructor-generated UUID, `options: ['default' => ...]`, literals |
|
|
211
|
-
| `TextChoices`, inline `choices=[...]` | `enum` | TypeScript `enum` plus `@Column({ type: 'enum', enum })` | backed `enum` in `src/Enum/` plus `enumType:` |
|
|
212
|
-
| `ForeignKey`, `OneToOneField` (`on_delete`, `related_name`, `null`) | `@relation(... onDelete: ...)` plus the reverse field | `@ManyToOne` / `@OneToOne` with `@JoinColumn` and `onDelete`, plus `@OneToMany` / inverse `@OneToOne` | `#[ORM\ManyToOne]` / `#[ORM\OneToOne]` with `#[ORM\JoinColumn(onDelete:)]`, plus `OneToMany(mappedBy:)` / inverse `OneToOne` |
|
|
213
|
-
| `ManyToManyField` | join model (`preserve`) or implicit relation (`normalize`) | `@ManyToMany` with `@JoinTable` and the inverse side | `#[ORM\ManyToMany]` with `#[ORM\JoinTable]` and the inverse side |
|
|
214
|
-
| `Meta.db_table`, `unique_together`, `indexes`, `UniqueConstraint`, `db_index` | `@@map`, `@@unique`, `@@index` | `@Entity('table')`, `@Unique([...])`, `@Index([...])` | `#[ORM\Table(name:)]`, `#[ORM\Index]`, `#[ORM\UniqueConstraint]` |
|
|
215
|
-
| Abstract base classes, multi-table inheritance | fields inherited / one-to-one primary key | fields inherited / one-to-one primary key | fields inherited / `#[ORM\Id]` on the one-to-one |
|
|
216
|
-
| Proxy models (`Meta.proxy`), `Meta.swappable`, `models.CompositePrimaryKey` | merged into the concrete model / resolved / `@@id` | merged into the concrete model / resolved / composite `@PrimaryColumn` | merged into the concrete model / resolved / several `#[ORM\Id]` |
|
|
217
|
-
|
|
218
|
-
TypeORM output is a single TypeScript file of entity classes. Relations use `Relation<T>` so entities declared in one file do not trip over circular references, and `--provider` picks the column types (`timestamptz` and `jsonb` for PostgreSQL, `datetime` and `json` for MySQL, `simple-json` for SQLite). Use `--naming normalize` for camelCase properties mapped to snake_case columns with `name:` options. `.ts` is not inferred as a format, so pass `--from typeorm` or `--to typeorm`. Reading uses decorators only (see Limitations). TypeORM entities are parsed statically with tree-sitter (no `reflect-metadata`, TypeScript compiler, or database needed), and the same naming modes apply to the result. The tests compile the emitted entities with `tsc`, build TypeORM's metadata for every provider and synchronize the SQLite output into an in-memory database when TypeORM is installed (`TYPEORM_DIR`, see `test/README.md`).
|
|
219
|
-
|
|
220
|
-
Doctrine (`--from doctrine`) reads PHP 8 entity classes with the tree-sitter PHP grammar, so no PHP, Composer or database is needed. Pass the entity directory (one class per file is typical; enums and mapped superclasses can live in other files) or individual files; `.php` is not claimed because Laravel uses it too. Doctrine's default underscore naming is assumed for anything the attributes leave out: the table is the snake_cased class name, the column the snake_cased property, a join column `<property>_id`, an embedded column `<property>_<column>`.
|
|
221
|
-
|
|
222
|
-
| Doctrine (PHP 8 attributes) | Read as |
|
|
223
|
-
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
224
|
-
| `#[ORM\Entity]` (`repositoryClass` ignored), `#[ORM\Table(name:)]` | a model; table `name` or the snake_cased class name |
|
|
225
|
-
| `#[ORM\Column]` `type` (or a `Types::` constant), `length`, `precision`, `scale`, `nullable`, `unique`, `name` | string (length 255 by default), text, int, bigint, decimal (10,0 by default), float, boolean, date and time types, json, binary, guid |
|
|
226
|
-
| `#[ORM\Column]` without `type` | inferred from the PHP type, as Doctrine does (`int`, `string`, `bool`, `float`, `array`, `DateTime`, `DateTimeImmutable`, `DateInterval`, backed enums) |
|
|
227
|
-
| `options: ['default' => ...]` | literal, `now` (`CURRENT_TIMESTAMP`), `uuid` (`gen_random_uuid()`) or enum member defaults |
|
|
228
|
-
| `#[ORM\Id]`, `#[ORM\GeneratedValue(strategy:)]`, `#[ORM\CustomIdGenerator]` | primary key; AUTO/IDENTITY/SEQUENCE become auto-increment, UUID (and Symfony `UuidGenerator`) a UUID key, NONE no default |
|
|
229
|
-
| several `#[ORM\Id]` | composite primary key |
|
|
230
|
-
| `#[ORM\ManyToOne]`, `#[ORM\OneToOne]` with `#[ORM\JoinColumn(name:, referencedColumnName:, nullable:, onDelete:)]` | foreign key / one-to-one (column `<property>_id`, nullable unless `nullable: false`) |
|
|
231
|
-
| `#[ORM\OneToMany]`, inverse `#[ORM\OneToOne]`, `mappedBy` / `inversedBy` | the reverse accessor of the owning relation |
|
|
232
|
-
| `#[ORM\ManyToMany]` (owning side) | many-to-many (custom `#[ORM\JoinTable]` names are not preserved) |
|
|
233
|
-
| `#[ORM\Index]`, `#[ORM\UniqueConstraint]` (class level or inside `#[ORM\Table]`), `Column(unique:, index:)` | indexes, unique constraints (a single unnamed unique column becomes a unique field), `fulltext` flag |
|
|
234
|
-
| backed enums (`enum Status: string`) used through `enumType` or the property type | an enum plus an enum-backed column |
|
|
235
|
-
| `#[ORM\Embeddable]` / `#[ORM\Embedded(columnPrefix:)]` | the embeddable's columns flattened into the owner, prefixed as Doctrine does |
|
|
236
|
-
| `#[ORM\MappedSuperclass]` and traits | columns, relations and indexes inherited |
|
|
237
|
-
| `#[ORM\InheritanceType('SINGLE_TABLE')]`, `#[ORM\DiscriminatorColumn]` | subclasses merged into the root table (nullable columns) plus a discriminator column |
|
|
238
|
-
| `#[ORM\InheritanceType('JOINED')]` | child table with a one-to-one primary key to the parent |
|
|
239
|
-
| `#[ORM\Version]` | integer column defaulting to 1 (optimistic locking is not represented) |
|
|
240
|
-
| `#[ORM\PreUpdate]` method assigning `$this->prop = new \DateTimeImmutable();` | the field is "updated automatically"; `#[ORM\PrePersist]` alone means a `now` default. Any other statement in a callback is ignored with a warning |
|
|
241
|
-
| `__construct` assignments and property initialisers (`$this->id = self::generateUuid();`, `= Status::Draft`, `= []`, `= 0`, `= new \DateTimeImmutable()`) | the field default (uuid, enum member, empty JSON, literal, now); a column `options: ['default' => ...]` wins; values that cannot be evaluated warn |
|
|
242
|
-
| `#[ORM\HasLifecycleCallbacks]`, other lifecycle callbacks, `#[ORM\Cache]`, `#[ORM\OrderBy]`, other libraries' attributes | ignored without a warning |
|
|
243
|
-
|
|
244
|
-
Doctrine output (`--to doctrine`) writes a directory, so pass `-o <directory>`. It follows PSR-4: one PHP 8.1+ file per entity (`src/Entity/Post.php`) and one backed enum per enum (`src/Enum/PostStatus.php`), each with `declare(strict_types=1);` and `use Doctrine\ORM\Mapping as ORM;`. `.php` is not inferred, so pass `--to doctrine`.
|
|
245
|
-
|
|
246
|
-
- **Namespace and paths.** The default namespace is `App\Entity` (enums in `App\Enum`). `--namespace Acme\Blog\Entity` (or `namespace` in the config file, or the `namespace` option of `emitDoctrine` / `convertText`) writes `src/Blog/Entity/Post.php` and puts the enums in `Acme\Blog\Enum`: the first namespace segment maps to `src/`, the rest to folders, and a namespace not ending in `Entity` gets a child `Enum` namespace.
|
|
247
|
-
- **Properties.** Typed and nullable (`?int`, `?string`, `DateTimeImmutable`, `bool`), private, with explicit `type:`, `length:`, `precision:` / `scale:`, `nullable:`, `unique:` and the column `name:`. Defaults are PHP property initializers plus `options: ['default' => ...]`; `now` defaults and UUIDs are set in the constructor (no extra packages: UUID keys and defaults use a small private `generateUuid()` helper). Auto-updated timestamps get `#[ORM\PrePersist]` / `#[ORM\PreUpdate]` lifecycle callbacks. Enum columns use `enumType:` with the PHP backed enum, and a default becomes the enum case.
|
|
248
|
-
- **Keys.** An auto-increment key is `#[ORM\Id]` with `#[ORM\GeneratedValue(strategy: 'AUTO')]`; composite keys repeat `#[ORM\Id]`, and a relation that is the primary key (multi-table inheritance) carries the `#[ORM\Id]` itself.
|
|
249
|
-
- **Relations.** Every relation is written on both sides with matching `inversedBy:` / `mappedBy:`: `ManyToOne` with `JoinColumn(name:, referencedColumnName:, nullable:, onDelete:)` and an inverse `OneToMany`, `OneToOne` with an inverse `OneToOne`, and `ManyToMany` with `JoinTable`, `JoinColumn`, `InverseJoinColumn` and an inverse `ManyToMany`. To-many properties are `Collection` (docblock `Collection<int, Post>`) initialised with `ArrayCollection` in the constructor.
|
|
250
|
-
- **Accessors.** Getters and fluent `static` setters (`isFeatured()` for booleans); to-many properties get `getPosts()`, `addPost()` and `removePost()` that keep the owning and inverse sides in sync.
|
|
251
|
-
- **Tables and indexes.** `#[ORM\Table(name:)]` plus class-level `#[ORM\Index(columns: [...])]` and `#[ORM\UniqueConstraint(columns: [...])]` on database column names. (`Table(indexes:, uniqueConstraints:)` has no effect in Doctrine ORM 3, so it is not used; ORM 2.19 or newer and PHP 8.1 or newer are required.) Reserved SQL words used as table or column names (`user`, `order`, ...) are written with Doctrine's backtick quoting.
|
|
252
|
-
- **Naming.** `preserve` keeps the property names of the source (`published_at`) and always writes the table and column names explicitly; `normalize` writes camelCase properties (`publishedAt`) mapped to snake_case columns. Getter and setter names are always camelCase (`getPublishedAt()`).
|
|
253
|
-
- **Lossy or unsupported.** Arrays and HStore become `json` columns; ranges become strings; durations become `dateinterval` (an ISO 8601 string); `GeneratedField` expressions, views, composite foreign keys, `Unsupported(...)` types, full-text indexes, `@@schema` and other Prisma-only constructs are written as plain tables and columns; a relation to a model without a single-column primary key is skipped; each case warns with the model and field. `onUpdate` actions have no Doctrine equivalent. A relation that points at a non-primary-key column (`to_field`) is written, but Doctrine's `orm:validate-schema` rejects it, and the warning says so. Doctrine ORM is relational, so `--provider mongodb` only warns.
|
|
254
|
-
- **Reading.** `--from doctrine` reads the same attributes back, the lifecycle callbacks and the constructor and initializer defaults as well, so Doctrine round trips through the shared model. PHP has one empty array literal, so an empty JSON object default (`{}`) comes back as `[]`.
|
|
255
|
-
|
|
256
|
-
Laravel output (`--to laravel`) writes a directory, so pass `-o <directory>` (the project root of a Laravel 11 or 12 app, for example `-o .`). `.php` is not inferred, so pass `--to laravel` (and `--from laravel` to read a project back, described below). Existing files with the same name are overwritten (use `--check` or `--dry-run` first); the migration file names are fixed (see below), so running the conversion again rewrites the same files rather than adding new ones.
|
|
257
|
-
|
|
258
|
-
- **Files.** One migration per table in `database/migrations/` (anonymous `return new class extends Migration` with `up()` and `down()` = `Schema::dropIfExists`), one Eloquent model per model in `app/Models/` (namespace `App\Models`) and one backed enum per enum in `app/Enums/` (`App\Enums`). `--namespace Acme\Blog\Models` (or `namespace` in the config file, or the `namespace` option of `emitLaravel` / `convertText`) writes `app/Blog/Models/Post.php` and puts the enums in `Acme\Blog\Enums`; the first namespace segment maps to `app/`.
|
|
259
|
-
- **Migration order and names.** Files are named `2024_01_01_000001_create_posts_table.php`, `..._000002_...`, counting up by one, with the date fixed so the output is deterministic. Tables are ordered so a foreign key always points at a table created earlier. A self reference is written inline (`constrained()` on the same table). A real dependency cycle (A needs B, B needs A) creates the tables first and adds the constraints that could not be written in a later `add_foreign_keys_to_<table>_table` migration, whose `down()` drops them. A fresh Laravel app already has a `users` migration, so a `User` model produces a second `create_users_table`: delete one of them.
|
|
260
|
-
- **Columns.** `id()` for an auto-increment key (`bigIncrements('code')` when the key is not called `id`; an `int` key becomes a big integer, like Laravel's own `id()`, and foreign keys use `foreignId`), `uuid()->primary()` or `ulid()->primary()` only when the schema says so (with `HasUuids` / `HasUlids` and `foreignUuid` / `foreignUlid` references), `string(n)`, `text`, `integer`, `bigInteger`, `unsignedInteger` and the other sized integers when a Prisma native type (`@db.UnsignedInt`, ...) says so, `double`, `decimal(p, s)`, `boolean`, `dateTime` / `dateTimeTz` / `date` / `time`, `jsonb` on PostgreSQL and `json` elsewhere, `binary`, `ipAddress`, `enum('status', [...])`, `nullable()`, `unique()`, `default(...)` and `useCurrent()` for `now` defaults. A `created_at` + `updated_at` pair becomes `timestamps()` (Eloquent maintains it) and a nullable `deleted_at` becomes `softDeletes()` with `use SoftDeletes`. Indexes and unique constraints are `index([...], 'name')` / `unique([...])`, and composite keys `primary([...])`.
|
|
261
|
-
- **Foreign keys.** `foreignId('author_id')->constrained('auth_user')->cascadeOnDelete()` with the matching helper for each action (`restrictOnDelete()`, `nullOnDelete()`, `cascadeOnUpdate()`, ...; `set default` through `onDelete('set default')`). A reference to a non-id column or to a string key is written as `$table->foreign('col')->references('col')->on('table')`.
|
|
262
|
-
- **Many-to-many.** A pivot table with both foreign keys and a composite primary key. `normalize` follows Laravel's convention (`post_tag`: singular model names in alphabetical order, `post_id` and `tag_id`) so `belongsToMany(Tag::class)` needs no extra arguments; `preserve` keeps the `<table>_<relation>` name of the other formats and passes the table and key names explicitly. A self relation uses `from_user_id` / `to_user_id`. When the source already models the join table (Prisma's explicit join model), it is an ordinary table and model with `belongsTo` / `hasMany`.
|
|
263
|
-
- **Models.** `$table`, `$primaryKey`, `$keyType`, `$incrementing` and `$timestamps` are written only when they differ from what Laravel would guess (`$table` is also written when the plural of the model name is not certain: `Person` is fine, `Axis` is written out). `$fillable` lists every writable column except generated keys and `timestamps()` columns; `$casts` has backed enums, `boolean`, `array` (json columns, arrays, hstore), `datetime`, `date`, `float` and `decimal:N`. Every relation is written on both sides with typed methods and generics docblocks: `belongsTo` (and `hasMany` / `hasOne` on the target), `belongsToMany` on both models, with explicit keys only when they are not Laravel's (`belongsTo(User::class, 'writer_user_id')`). A method that would clash with a column or an Eloquent method gets a `Relation` suffix and a warning.
|
|
264
|
-
- **Naming.** `preserve` keeps table and column names exactly (written as `$table` and in the migrations); `normalize` uses plural snake_case tables (`blog_posts`), snake_case columns, `<relation>_id` keys, adds `created_at` / `updated_at` where missing and keeps auto-increment keys as they are. Relationship methods are always camelCase (`editedPosts()`).
|
|
265
|
-
- **Lossy or unsupported.** Arrays and HStore become `json` columns, ranges and durations become strings, `GeneratedField` expressions (Python, not SQL), views, composite foreign keys, `Unsupported(...)` types, full-text indexes, index options, `@@schema` and other Prisma-only constructs are written as plain tables and columns; a relation to a model without a single-column primary key is skipped; each case warns with the model and field. Eloquent has no composite primary keys: the migration gets `primary([...])` and the model a warning. An auto-updated column that is not part of `timestamps()` is only refreshed by MySQL (`useCurrentOnUpdate()`). Laravel does not map PostgreSQL-specific types, so `--provider` only chooses `jsonb` versus `json` and the MySQL form of JSON defaults.
|
|
266
|
-
- **Not converted.** Policies, factories, seeders, observers, morph relations and anything else outside the schema are not generated, and the models extend `Illuminate\Database\Eloquent\Model` (not `Authenticatable`).
|
|
267
|
-
|
|
268
|
-
GORM output (`--to gorm`) is write-only and writes a directory, so pass `-o <directory>`: one Go file per model (`models/post.go`, snake_case) and one per enum (`models/post_status.go`), all in the same package, each importing only what it uses and formatted exactly as `gofmt` would (the tests run `gofmt -l`, `go vet` and a SQLite `AutoMigrate` with the real GORM when Go is installed). `.go` is not inferred, so pass `--to gorm`.
|
|
269
|
-
|
|
270
|
-
- **Package and paths.** The package is `models` and the files go in a directory of that name; `--go-package store` (or `goPackage` in the config file, or the `goPackage` option of `emitGorm` / `convertText`) writes `store/post.go` with `package store`. The name must be a Go identifier that is not a keyword.
|
|
271
|
-
- **Fields.** Exported, typed fields with a `gorm:"..."` tag: `string` (`size:n`), `string` with `type:text`, `int32`, `int64`, `float64`, `bool`, `time.Time` (`type:date` for dates), `uuid.UUID` ([google/uuid](https://github.com/google/uuid), `type:uuid` on PostgreSQL, `char(36)` on MySQL), `decimal.Decimal` ([shopspring/decimal](https://github.com/shopspring/decimal) with `type:decimal(p,s)`), `datatypes.JSON` ([gorm.io/datatypes](https://github.com/go-gorm/datatypes)) and `[]byte`. A nullable column is a pointer (`*string`, `*time.Time`, `*uuid.UUID`); `datatypes.JSON` and `[]byte` hold nil themselves. Tags use `column:` (only when GORM would derive another name from the field), `size:`, `primaryKey`, `autoIncrement` (`autoIncrement:false` for a lone integer key without one, because GORM would otherwise add it), `not null`, `unique`, `default:`, `autoCreateTime` / `autoUpdateTime` and the index tags below. A field name is the IR name in Go style (`public_id` becomes `PublicID`).
|
|
272
|
-
- **Defaults.** Literal and enum defaults are `default:` tags. A `now` default is `autoCreateTime` and an auto-updated column is `autoUpdateTime`. A UUID default is `default:gen_random_uuid()` on PostgreSQL and a generated `BeforeCreate` hook (`uuid.New()`, or `uuid.NewV7()` for version 7) everywhere else, so the key is known after `Create` on every database.
|
|
273
|
-
- **gorm.Model and soft delete.** A model with exactly the `gorm.Model` columns (a big-integer auto-increment `id`, date-time `created_at` and an auto-updated `updated_at`, and a nullable, indexed `deleted_at`) embeds `gorm.Model`, with its other fields after it. A nullable date-time named `deleted_at` on any other model is a `gorm.DeletedAt` field, with a warning, because GORM then hides rows where it is set.
|
|
274
|
-
- **Relations.** Every relation is written on both sides. A foreign key is a typed `<Relation><Key>` field (`AuthorID`) plus the association (`Author User` for a required parent that cannot make the type recursive, otherwise `*User`), tagged `foreignKey:AuthorID` (and `references:` for a non-key target) and `constraint:OnDelete:CASCADE` / `OnUpdate:...`, on both the belongs-to and the has-one (`*Profile`) or has-many (`[]Post`) side. Reverse names come from `related_name` or the plural of the model (`Posts`). Many-to-many is `[]Tag` with `many2many:<table>_<relation>`, plus `joinForeignKey` / `joinReferences` when the join columns differ from GORM's (always for a self relation), and `OnDelete:CASCADE` on both sides, since GORM builds the join table from whichever struct it migrates first. Keys that are relations (multi-table inheritance, composite keys of foreign keys) get `primaryKey` on the foreign key field.
|
|
275
|
-
- **Tables and indexes.** `func (Post) TableName() string` only when the table is not what GORM derives from the type name (`blog_post`, but not `posts`), using GORM's own naming and pluralization rules (checked against `gorm.io/gorm` in the tests). Indexes are tags on the fields: `index` / `uniqueIndex`, `index:name`, and for composite indexes a shared name with `priority:n` (`idx_<table>_<columns>` when the source has no name).
|
|
276
|
-
- **Enums.** A typed string (`type PostStatus string`) with a `const` block in its own file; labels become trailing comments. The column has the enum type and a `default:` for an enum default.
|
|
277
|
-
- **Naming.** `preserve` keeps the source's table and column names (explicit `TableName()` / `column:` wherever GORM's convention would differ). `normalize` applies GORM's conventions: plural snake_case tables (`posts`, join tables keep their names), snake_case columns, and `created_at` / `updated_at` where missing; primary keys keep their type (unlike Prisma output, no UUID keys are introduced).
|
|
278
|
-
- **Lossy or unsupported.** Integers are signed `int32` / `int64` (the IR has no unsigned types). Arrays become `datatypes.JSONSlice[T]`, HStore `datatypes.JSON`, durations `time.Duration` (nanoseconds in a bigint), ranges strings; generated columns, views, composite foreign keys, `Unsupported(...)` columns (written as `string` with `type:<name>`), full-text indexes and index options, `@@schema` and other Prisma-only constructs are written as plain tables and columns; a relation to a model without a single-column primary key is skipped; each case warns with the model and field. The IR has no check constraints, column comments or polymorphic relations, so no `check:`, `comment:` or `polymorphic:` tags are written. GORM is relational, so `--provider mongodb` only warns. The output needs `gorm.io/gorm`, plus `github.com/google/uuid`, `github.com/shopspring/decimal` and `gorm.io/datatypes` for the types that use them (`go get` them in your module).
|
|
279
|
-
|
|
280
|
-
Drizzle output (`--to drizzle`) is write-only and writes a directory, so pass `-o <directory>`: `schema.ts` (every enum, table and `relations()` in one file) and `drizzle.config.ts` (`defineConfig` with the matching `dialect`, `schema: './schema.ts'` and `out: './drizzle'`, so `npx drizzle-kit generate` works next to it). One file keeps every table in reach of the others and is what `drizzle-kit` reads without a glob; the tables are ordered so a table follows the tables it references, which keeps circular references compiling without splitting them across modules. `.ts` is not inferred, so pass `--to drizzle`. `drizzle-orm` 0.36 or newer is needed (the table callback returns an array), and `drizzle-kit` for the config.
|
|
281
|
-
|
|
282
|
-
- **Dialects.** `--provider` picks `drizzle-orm/pg-core` (`postgresql`, `cockroachdb`), `mysql-core` (`mysql`) or `sqlite-core` (`sqlite`); each gets its own table function (`pgTable`, `mysqlTable`, `sqliteTable`) and only the builders it uses are imported. Drizzle has no SQL Server or MongoDB dialect, so those two write PostgreSQL tables and warn.
|
|
283
|
-
- **Columns.** Typed builders with the database name as the first argument: `varchar({ length })` (`text` when the source has no length; MySQL needs a length and gets 255), `text`, `integer` / `int`, `bigint({ mode: 'number' })`, `doublePrecision` / `double` / `real`, `numeric` / `decimal({ precision, scale })`, `boolean`, `timestamp({ withTimezone: true })` / `datetime`, `date`, `time`, `uuid` / `char(36)`, `jsonb` / `json`, `inet`, `interval`. On SQLite dates are `integer({ mode: 'timestamp' })`, booleans `integer({ mode: 'boolean' })`, JSON `text({ mode: 'json' })`, decimals `numeric` and UUIDs `text`. `.notNull()` on required columns, `.unique()` (with the constraint name when the source has one), `.array()` for PostgreSQL arrays, and `customType` declarations at the top of the file for the PostgreSQL types Drizzle has no builder for (`bytea`, `hstore`, ranges).
|
|
284
|
-
- **Keys and defaults.** An auto-increment key is `serial` / `bigserial` (PostgreSQL), `.autoincrement()` (MySQL) or `.primaryKey({ autoIncrement: true })` (SQLite). A UUID key is `uuid().defaultRandom()`, `.default(sql`(UUID())`)` on MySQL and `$defaultFn(() => crypto.randomUUID())` on SQLite (with a warning: that default only runs for rows inserted through Drizzle). `now` is `.defaultNow()`, `CURRENT_TIMESTAMP` or `(unixepoch())`; literals are `.default(...)` (JSON as an object literal, decimals as strings, date-times as `new Date(...)`); an auto-updated column is `.$onUpdate(() => new Date())`.
|
|
285
|
-
- **Enums.** Every enum exports its values (`export const postStatusValues = [...] as const`) and a union type. PostgreSQL adds `pgEnum('name', values)`, MySQL uses `mysqlEnum('column', values)` on the column, SQLite uses `text('column', { enum: values })` (TypeScript only; SQLite gets no check constraint).
|
|
286
|
-
- **Relations.** A foreign key is `.references(() => table.id, { onDelete, onUpdate })` (the NO ACTION default is left out; MySQL has no SET DEFAULT and warns), typed after the column it points at (a UUID key gives a `uuid` foreign key). A reference to a table declared later, and a self reference, is annotated `(): AnyPgColumn =>` (`AnyMySqlColumn`, `AnySQLiteColumn`) so TypeScript does not report a circular initializer. `relations()` is written for both sides of every relation: `one()` with `fields` and `references` on the owner, `many()` (`one()` for a one-to-one) on the target, named after `related_name` or the plural of the model; `relationName` is added to both sides when two relations join the same tables or a table points at itself. A many-to-many relation becomes the explicit join table Django creates (a surrogate `id`, two cascading foreign keys, a unique pair) in both naming modes.
|
|
287
|
-
- **Indexes and keys.** `index('name').on(table.a, table.b)`, `uniqueIndex(...)` and `primaryKey({ columns: [...] })` (composite keys) in the table callback; unnamed indexes are named `<table>_<columns>_idx` / `_key`, shortened to 63 characters.
|
|
288
|
-
- **Names.** TypeScript keys are camelCase in both modes (`createdAt`); tables are camelCase variables of the model name with `export type Post` / `NewPost` row types. `preserve` keeps the source's table, column and enum names as the string arguments; `normalize` applies the fresh-schema style (singular snake_case tables, UUID keys, `created_at` / `updated_at`, snake_case columns).
|
|
289
|
-
- **Lossy or unsupported.** Arrays, hstore and ranges outside PostgreSQL become JSON, text or varchar columns and durations integers (microseconds); generated columns (Python), composite foreign keys, views (written as tables), index types, sort orders and operator classes, full-text indexes, `Unsupported(...)` columns and `@@schema` are written as plain tables and columns; client-generated (`cuid()`) and database-expression defaults are dropped; each case warns with the model and field. The IR has no check constraints or column comments, so none are written. The tests compile the output with `tsc --strict`, run `drizzle-kit generate` for all three dialects, `drizzle-kit push` into SQLite and compare the result (and Drizzle's relational configuration) with the schema when Drizzle is installed (`DRIZZLE_DIR`, see `test/README.md`).
|
|
290
|
-
|
|
291
|
-
Graphene output (`--to graphene`) is write-only and pairs with Django models: it writes one Python file that imports every model from `.models` (generate it with `--to django`, or use your own models) and defines a `DjangoObjectType` per model with an explicit `Meta.fields` list (forward and reverse relations included), a `Query` with a single-item field (`post(id)`) and a list field (`post_list`) per model, `Create`/`Update`/`Delete` mutations built on `graphene.Mutation`, and a final `schema = graphene.Schema(query=Query, mutation=Mutation)`. Inputs map the scalar types to `String`, `Int`, `BigInt`, `Float`, `Decimal`, `Boolean`, `DateTime`, `Date`, `Time`, `UUID` and `JSONString`; a field is required when it is not nullable and has no default, and relations are `ID` inputs (`author_id`, many-to-many `tags_ids`). Enum-backed fields rely on graphene-django's choice conversion for output and are `String` inputs validated by the model. Binary columns and relations to models outside the schema are left out, and models with a composite primary key get only a list query and a create mutation; each case produces a warning naming the model and field. Field names are snake_case like the Django output, `.py` stays owned by Django so pass `--to graphene`, and the naming mode has no effect. Requires graphene-django 3.x at runtime. The tests import the generated schema next to the generated Django models (or, for a Django source, the source models with `django.contrib.auth` installed as the user model) with the real Django and graphene-django when they are installed (`DJANGO_PYTHON`), build it, run the introspection query and create, read, update and delete rows through it.
|
|
292
|
-
|
|
293
|
-
Django-only constructs: a proxy model (`Meta.proxy = True`) has no table of its own, so it is merged into its concrete model (chains of proxies are followed) and relations that target it are pointed at the concrete model, with a warning. A model with `Meta.swappable = "AUTH_USER_MODEL"` is used as the target of `settings.AUTH_USER_MODEL` and `get_user_model()` instead of an assumed `User`. Custom managers (`objects = MyManager()`, `Manager.from_queryset(...)()`, `QuerySet.as_manager()`) and the classes behind them are ignored without warnings. `django.contrib.postgres` arrays, HStore and range fields, and `GeneratedField` / `db_default` (Django 5.0+), round-trip through `--to django`; the Django emitter writes the `django.contrib.postgres.fields` import and notes the required Django version in a warning.
|
|
294
|
-
|
|
295
|
-
Prisma → Django applies the reverse mapping. Field names are converted from camelCase to snake_case, with `db_column` set when the column name differs.
|
|
296
|
-
|
|
297
|
-
### Laravel (`--from laravel`)
|
|
298
|
-
|
|
299
|
-
Laravel keeps its schema in migrations, not in the models, so `--from laravel` works in two steps, with the tree-sitter PHP grammar (no PHP, Composer or database is needed). First it replays the migration files in order (sorted by file name, which starts with the timestamp) to rebuild the **current** schema: a column added by one migration, renamed by the next and dropped by a third is handled the way the database would. Then it reads the Eloquent models for what the migrations do not say: relation names and their reverse names, casts to backed enums, custom table names and timestamp handling. A table without a model is still converted (its model is named after the singular table name), and `down()` is ignored.
|
|
300
|
-
|
|
301
|
-
Pass the project root (only `database/migrations` and `app/` are read; `vendor/`, `storage/`, `tests/`, `bootstrap/cache` and `node_modules/` are skipped), or the migrations and model directories separately: `ormbridge convert -i ./database/migrations ./app/Models ./app/Enums --from laravel`. Files are recognised by what they contain, not by where they live: a class whose `up()` calls `Schema::`, a class that extends `Model` (or `Authenticatable`, or another model) and a backed enum, so a flat folder works too. Anonymous (`return new class extends Migration`) and named migration classes are both read. `.php` is not claimed, so pass `--from laravel`. Laravel output (`--to laravel`) is described above.
|
|
302
|
-
|
|
303
|
-
| Laravel migration | Read as |
|
|
304
|
-
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
305
|
-
| `Schema::create`, `Schema::table`, `drop`, `dropIfExists`, `rename`, `dropColumns` | tables are created, altered, dropped and renamed in migration order |
|
|
306
|
-
| `$table->dropColumn`, `renameColumn`, `->change()` | the column is removed, renamed (also inside indexes and keys) or restated (`change()` replaces the definition, as Laravel does) |
|
|
307
|
-
| `dropIndex`, `dropUnique`, `dropFullText`, `dropPrimary`, `dropForeign`, `renameIndex` | by name or by column list, using Laravel's default index names (`posts_title_index`) |
|
|
308
|
-
| `dropTimestamps`, `dropSoftDeletes`, `dropRememberToken`, `dropMorphs`, `dropConstrainedForeignId` | the columns they added are removed |
|
|
309
|
-
| `id`, `bigIncrements`, `increments` and the tiny / small / medium variants, `integer(col, true)` | auto-increment primary key (`BigInt` for `id` and `bigIncrements`) |
|
|
310
|
-
| `string`, `char`, `text`, `mediumText`, `longText`, `tinyText` | `string` with its length (255 by default), `text` |
|
|
311
|
-
| `integer`, `tinyInteger`, `smallInteger`, `mediumInteger`, `bigInteger` and the `unsigned*` variants, `year` | `int` / `bigInt` (unsigned is not tracked) |
|
|
312
|
-
| `boolean`, `decimal(col, total, places)`, `float`, `double`, `unsignedDecimal` | `boolean`, `decimal` with precision and scale (8, 2 by default), `float` |
|
|
313
|
-
| `date`, `dateTime`, `dateTimeTz`, `time`, `timeTz`, `timestamp`, `timestampTz` | `date`, `dateTime`, `time` |
|
|
314
|
-
| `timestamps`, `timestampsTz`, `nullableTimestamps`, `softDeletes`, `softDeletesTz`, `rememberToken` | expanded to `created_at` / `updated_at` (nullable), `deleted_at` (nullable), `remember_token` (`string(100)`, nullable) |
|
|
315
|
-
| `json`, `jsonb`, `binary`, `uuid`, `ulid`, `ipAddress` | `json`, `bytes`, `uuid`, `string(26)`, `ipAddress` |
|
|
316
|
-
| `enum(col, [...])` | a string column with an enum; a cast to a backed enum on the model supplies the enum name, otherwise one is created from the values (`PostStatus`) |
|
|
317
|
-
| `nullable`, `default` (literals, `DB::raw`, `now()`, `Carbon::now()`), `useCurrent`, `useCurrentOnUpdate`, `storedAs` / `virtualAs` | nullability, defaults (`CURRENT_TIMESTAMP` becomes `now`, `gen_random_uuid()` becomes a UUID default), auto-updated timestamps, generated columns |
|
|
318
|
-
| `unique`, `index`, `fullText`, `primary` (also with column lists and names), `->unique()` and `->index()` on a column | unique columns, indexes (a name is kept only when it is not Laravel's default), composite primary keys |
|
|
319
|
-
| `foreignId()->constrained()`, `foreignUuid`, `foreignUlid`, `foreignIdFor`, `foreign()->references()->on()`, `onDelete`, `onUpdate` | a relation with its column, nullability and referential actions (`cascadeOnDelete`, `restrictOnDelete`, `nullOnDelete`, `noActionOnDelete` and the update variants) |
|
|
320
|
-
| `comment`, `after`, `first`, `charset`, `collation`, `engine`, `invisible` | ignored without a warning |
|
|
321
|
-
|
|
322
|
-
| Eloquent model | Read as |
|
|
323
|
-
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
324
|
-
| `class Post extends Model` (also `Authenticatable`, abstract base models, `Pivot`) | the table is `$table`, or Laravel's convention: the snake_case plural of the class name (`UserProfile` becomes `user_profiles`); a model without a table is skipped with a warning |
|
|
325
|
-
| `$casts` / `casts()` to a string-backed enum (PHP 8.1), `boolean`, `array` / `json`, `decimal:2`, `datetime` | the field gets the enum (declared in any file), and the types the cast implies |
|
|
326
|
-
| `$timestamps`, `CREATED_AT`, `UPDATED_AT` | with timestamps on, the update column refreshes on every save and the creation column starts at the current time; with `$timestamps = false` nothing is assumed |
|
|
327
|
-
| `use SoftDeletes`, `HasUuids`, `HasUlids` | `deleted_at` is checked to exist; the primary key gets a generated UUID / ULID default |
|
|
328
|
-
| `belongsTo`, `hasMany`, `hasOne` (custom foreign and owner keys) | name the relation (`author`) and its reverse (`posts`); the foreign key is `<method>_id` for `belongsTo` and `<model>_id` for `hasOne` / `hasMany`, as in Laravel; a `hasOne` or a unique column makes it one-to-one |
|
|
329
|
-
| `belongsToMany` (pivot table, key and `withPivot` arguments) | a pivot table that holds only the two keys (and optionally an `id` and timestamps) becomes a many-to-many field; the default pivot name is the singular model names in alphabetical order (`post_tag`); a pivot with other columns stays a model |
|
|
330
|
-
|
|
331
|
-
Everything is taken from the migrations and the models together, so a relation can come from a foreign key constraint, from a `belongsTo`, or from the `hasMany` on the other side; a foreign key column with none of the three is a plain column.
|
|
332
|
-
|
|
333
|
-
### GORM (`--from gorm`)
|
|
334
|
-
|
|
335
|
-
GORM has no schema file: the schema is the Go structs and their `gorm:"..."` tags. `--from gorm` reads them with the tree-sitter Go grammar (no Go toolchain or database is needed, and no Go code is run). Pass the directory that holds the models, or the files; every `.go` file is read, except `*_test.go` and anything below `vendor/`, `testdata/`, `node_modules/` and `.git/`. `.go` is not claimed, so pass `--from gorm`. GORM is read only for now: there is no `--to gorm` yet.
|
|
336
|
-
|
|
337
|
-
GORM does not mark a model, so a struct is read as one when it embeds `gorm.Model`, has a `gorm:"..."` tag, has a field called `ID`, has a `TableName()` method, or is the target of an association from another model. A struct that is only embedded in others (a shared base) is not a model of its own; unexported fields, `gorm:"-"` fields and `-:migration` fields are not columns.
|
|
338
|
-
|
|
339
|
-
Names follow GORM's own conventions, so the database names are reproduced exactly: tables are the pluralised snake_case struct name (`BlogPost` becomes `blog_posts`; the plural rules are those of `jinzhu/inflection`, which GORM uses) unless `TableName()` returns another literal or constant, columns are the snake_case field name (`PublicID` becomes `public_id`, with GORM's initialisms such as `ID`, `URL` and `HTTP`) unless `column:` says otherwise, a field called `ID` is the primary key, and a foreign key is `<Field>ID` (`AuthorID` for the field `Author`). The names were checked against GORM v1.31 (`gorm.io/gorm/schema`) for the test fixtures. Struct names are the model names; field names become camelCase (`PublicID` becomes `publicId`).
|
|
340
|
-
|
|
341
|
-
| GORM | Read as |
|
|
342
|
-
| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
343
|
-
| `string`, `bool`, `int*`, `uint*`, `float*`, `[]byte` | `text` (`string` with a length when `size:` is given), `boolean`, `int` up to 32 bits and `bigInt` above (as in GORM's PostgreSQL dialect, so `int`, `int64`, `uint` and `uint32` are `bigInt`), `float`, `bytes` |
|
|
344
|
-
| `time.Time`, `time.Duration`, `uuid.UUID`, `decimal.Decimal`, `datatypes.JSON`, `datatypes.Date`, `net.IP` | `dateTime`, `bigInt`, `uuid`, `decimal`, `json`, `date`, `ipAddress` |
|
|
345
|
-
| pointers, `sql.NullString` and the other `sql.Null*` types, `sql.Null[T]`, `gorm.DeletedAt` | a nullable column; a plain (non-pointer) field is required, as the Go code treats it (AutoMigrate itself only adds `NOT NULL` for `not null` and primary keys) |
|
|
346
|
-
| `type:`, `size:`, `precision:`, `scale:`, `serializer:json` | the SQL type (`varchar(n)`, `numeric(p,s)`, `jsonb`, arrays such as `text[]`, ...), length, digits and scale; a float with a precision is a `decimal`; an unknown SQL type is an unsupported column |
|
|
347
|
-
| `column:`, `primaryKey`, `autoIncrement`, `unique`, `not null`, `default:` | column name, primary key (several make a composite key; an integer key auto-increments unless `autoIncrement:false`), unique, required, defaults (`now()`, `gen_random_uuid()`, literals, expressions) |
|
|
348
|
-
| `index`, `uniqueIndex` (named, composite with `priority:` or `composite:`), `sort:`, `length:`, `class:FULLTEXT`, `type:` | indexes; a single unnamed unique index becomes a unique field |
|
|
349
|
-
| `CreatedAt`, `UpdatedAt`, `autoCreateTime`, `autoUpdateTime` | a creation column defaulting to now, an update column refreshed on every save |
|
|
350
|
-
| `gorm.Model`, `embedded`, `embeddedPrefix:`, anonymous embedded structs | the columns flattened into the owner, in order, with the prefix applied to the column names (a field of the owner shadows a promoted field of the same name) |
|
|
351
|
-
| `gorm.DeletedAt` (soft delete) | a nullable timestamp column, with a warning that GORM's query filtering is not represented |
|
|
352
|
-
| belongs to, has one, has many (`foreignKey:`, `references:`, composite keys, `constraint:OnDelete:...,OnUpdate:...`) | a foreign key or one-to-one relation with its reverse name, found the way GORM finds it (by convention or by the tags); the foreign key field is replaced by the relation; composite keys become composite foreign keys |
|
|
353
|
-
| `many2many:table` (`joinForeignKey:`, `joinReferences:`), one or both sides declared | a many-to-many field with its reverse name; a join table or column name that differs from the derived one is not preserved (warning) |
|
|
354
|
-
| `type Status string` with typed constants | an enum (`StatusDraft` is the member `Draft`); the column keeps its length and an enum default |
|
|
355
|
-
| `TableName()` returning a literal or a constant | the table name |
|
|
356
|
-
|
|
357
|
-
### Drizzle ORM (`--from drizzle`)
|
|
358
|
-
|
|
359
|
-
Drizzle's schema is TypeScript code, so `--from drizzle` reads it with the tree-sitter TypeScript grammar (no Node project, `drizzle-orm` install or database is needed, and none of your code is run). Pass the folder that holds the schema, or the files; every `.ts` file is read (not `*.d.ts`, `*.test.ts`, `*.spec.ts` or anything below `node_modules/`) and only the tables, enums and `relations()` in them are used. `.ts` is TypeORM's extension, so Drizzle is never inferred. The dialect comes from the table function: `pgTable`, `mysqlTable` and `sqliteTable`, `pgSchema('x').table(...)` and `mysqlSchema(...)`, and tables made with `pgTableCreator((name) => `app_${name}`)` (a plain template prefix or suffix is applied). SingleStore, Gel, SQL Server and CockroachDB tables are reported and skipped. Drizzle has no table classes, so a model is named after the variable (`blogPosts` becomes `BlogPost`), the TS property key is the field name and the string argument of the column builder is the column name (when there is none, the key is used, so a `casing` option set when the database is opened is not seen).
|
|
360
|
-
|
|
361
|
-
| Drizzle | Read as |
|
|
362
|
-
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
363
|
-
| `integer`, `smallint`, `bigint({ mode })`, `int`, `tinyint`, `mediumint`, `year`, `serial`, `bigserial` | `int` or `bigInt` (PostgreSQL `serial` / `bigserial` are auto-increment and required; MySQL `serial` is an auto-increment, unique, required `bigInt`; `unsigned` is dropped with a warning) |
|
|
364
|
-
| `real`, `doublePrecision`, `float`, `double`, `numeric` / `decimal({ precision, scale })` | `float`, `decimal` with its digits and scale |
|
|
365
|
-
| `varchar`, `char`, `text`, `tinytext` to `longtext`, `boolean`, `uuid`, `json`, `jsonb`, `date`, `time`, `timestamp`, `datetime`, `interval`, `inet`, `binary`, `varbinary` | `string` (with `length`), `text`, `boolean`, `uuid`, `json`, `date`, `time`, `dateTime`, `duration`, `ipAddress`, `bytes`; `.array()` adds an array dimension |
|
|
366
|
-
| SQLite `integer`, `text`, `blob` with `{ mode }` | `integer`: `int`, `boolean` or `dateTime` (`'timestamp'`, with a warning that it is stored as Unix time); `text`: `text` or `json`; `blob`: `bytes`, `json` or `bigInt` |
|
|
367
|
-
| `point`, `line`, `geometry`, `vector`, `cidr`, `macaddr`, `bit`, custom types | an unsupported column with its SQL type, and a warning |
|
|
368
|
-
| `.notNull()`, `.primaryKey()`, `.unique(name?)`, `.autoincrement()`, `.generatedAlwaysAsIdentity()` | required (columns are nullable unless marked, as in Drizzle), primary key, unique (with its constraint name), auto-increment |
|
|
369
|
-
| `.default(...)`, `.defaultNow()`, `.defaultRandom()`, ``.default(sql`...`)`` | a literal, JSON text, enum member, `now`, generated UUID or SQL expression; a default that is not static is dropped with a warning |
|
|
370
|
-
| `.$defaultFn(...)`, `.$default(...)` | a client-side default: `new Date()` becomes `now` and `crypto.randomUUID()` a generated UUID (on date-time and UUID columns), anything else is dropped; always with a warning |
|
|
371
|
-
| `.$onUpdate(...)`, `.onUpdateNow()` | an auto-updated date-time column (a redundant `defaultNow()` is not kept, as for Prisma `@updatedAt`) |
|
|
372
|
-
| ``.generatedAlwaysAs(sql`...`, { mode })`` | a generated column (the expression is kept as written) |
|
|
373
|
-
| `pgEnum('name', [...])`, `schema.enum(...)`, `mysqlEnum('col', [...])` | an enum (member names are the upper-case snake spelling of the values; the database name and schema are kept); a `mysqlEnum` column gets an enum named after the table and column |
|
|
374
|
-
| `.references(() => users.id, { onDelete, onUpdate })`, single-column `foreignKey({ ... })` | a foreign key relation; the column is replaced by the relation (named by the `one(...)` entry of `relations()`, otherwise the column name without `Id`); a unique or primary key column makes it one-to-one |
|
|
375
|
-
| `relations(table, ({ one, many }) => ...)`, `relationName` | relation names and reverse sides: `one(target, { fields, references })` names the owning side, `many(target)` and a field-less `one(target)` name the reverse side, `relationName` tells several relations apart |
|
|
376
|
-
| composite `foreignKey({ columns, foreignColumns })`, composite `primaryKey({ columns, name })` | a composite foreign key (the columns stay plain fields) and a composite primary key |
|
|
377
|
-
| `index()`, `uniqueIndex()`, `unique()` with `.on(...)`, `.using('gin', ...)`, `.desc()`, `.op(...)` | indexes and unique constraints (a single unnamed unique becomes a unique field), with access method, sort order and operator class |
|
|
378
|
-
| a join table with two foreign keys and a composite key | stays a model, as for Prisma's explicit many-to-many; it is not folded into a many-to-many field |
|
|
379
|
-
| column objects shared with `...spread` (also from a function call), one-level column helpers | the columns are expanded in place |
|
|
380
|
-
|
|
381
|
-
### JSON Schema and OpenAPI (`--from json-schema`)
|
|
382
|
-
|
|
383
|
-
`--from json-schema` reads JSON Schema (draft 2020-12, 2019-09 and 7) and the `components.schemas` of OpenAPI 3.x (and Swagger 2 `definitions`) documents. It is plain JSON, so nothing is installed or run. Only JSON is read: ormbridge has no YAML parser, so convert a YAML OpenAPI document to JSON first (the error says so). It is read only for now (there is no `--to json-schema` yet). `.json` is too generic to infer and `.schema.json` is not an extension the registry can match (it looks at the last extension only), so pass `--from json-schema`. A directory is read recursively: every `.json` file, except tool configuration (`package.json`, `tsconfig*.json`, ...) and anything below `node_modules/`, `vendor/`, `dist/`, `build/`, `coverage/` and `.git/`.
|
|
384
|
-
|
|
385
|
-
Every object schema with properties under `$defs`, `definitions` or `components.schemas` is a model (named after its key), and so is a root object schema (named after its `title`, `$id` or file name). Enums, aliases of scalar types and free-form objects are not models.
|
|
386
|
-
|
|
387
|
-
| JSON Schema | Read as |
|
|
388
|
-
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
389
|
-
| `properties`, `required` | the fields; a property that is not in `required`, has `type: [..., "null"]`, `nullable: true` (OpenAPI 3.0), an `anyOf` / `oneOf` with `{"type": "null"}` or an `enum` with `null` is nullable |
|
|
390
|
-
| `type: string` without `maxLength`; with `maxLength` | `text`; a `string` of that length |
|
|
391
|
-
| `format`: `date-time`, `date`, `time`, `uuid`, `duration`, `ipv4`, `ipv6`, `byte` / `binary` / `contentEncoding: base64` | `dateTime`, `date`, `time`, `uuid`, `duration`, `ipAddress`, `bytes`; other formats (`email`, `uri`, ...) stay strings |
|
|
392
|
-
| `type: integer`, `format: int32` / `int64`; `type: number`, `format: float` / `double` | `int` / `bigInt`; `float` |
|
|
393
|
-
| `format: decimal` (on a string or a number), `x-precision`, `x-scale`, `multipleOf: 0.01` | `decimal` with digits and places |
|
|
394
|
-
| `boolean`; `object`, an untyped schema, `{}` | `boolean`; `json` |
|
|
395
|
-
| `array` of scalars (nested arrays too) | an array column of that type |
|
|
396
|
-
| `enum` of strings, `oneOf` / `anyOf` of `{const, title}`, `x-enum-varnames` | an enum named after the `$defs` entry it is defined in, otherwise after the property (`account_state` becomes `AccountState`); member names are the upper-case value (`in-progress` becomes `IN_PROGRESS`), labels come from `title`. The same values share one enum, a clash gets the model name as a prefix |
|
|
397
|
-
| `default` | a literal default (a JSON default is stored as text, an enum default names the member); a date-time default of `now`, `now()` or `CURRENT_TIMESTAMP` is the current time |
|
|
398
|
-
| `readOnly: true` | generated by the database: an integer primary key counts up, a uuid is generated, a date-time is set on insert (`updated...` names are refreshed on every save) |
|
|
399
|
-
| `x-unique`, `x-index`, `x-indexes: [{fields, unique, name}]` (model), `x-table-name` (model) | a unique column, indexes, the table name (the model name otherwise) |
|
|
400
|
-
| a property named `id` or `<model>Id`, `x-primary-key` (property, several make a composite key, or a model-level array of names) | the primary key; a model without one gets an integer `id` and a warning |
|
|
401
|
-
| `$ref` to another model (also inside `allOf: [{...}]`, `anyOf` with null, or with sibling keywords) | a foreign key (required means not null; `x-on-delete`: `cascade`, `set null`, `restrict`, `no action`, `set default`; `x-related-name`) named `<property>_id`, or the declared `authorId` / `author_id` property next to it |
|
|
402
|
-
| an array of `$ref` to a model that references this one | the reverse side of that foreign key (its `relatedName`); with several candidates `x-related-name` on the foreign key picks one |
|
|
403
|
-
| arrays of each other on both models | a many-to-many field on the model defined first |
|
|
404
|
-
| single references that point at each other | a one-to-one relation; the required side holds the key (a warning when that decides nothing) |
|
|
405
|
-
| `$ref` pointers (`#/$defs/X`, `#/definitions/X`, `#/components/schemas/X`, `#`), `$id`-relative references and relative file references between the input files | followed; a remote reference that is not an input file, a missing target, a named anchor or a reference cycle that never reaches an object keeps the property as a `json` column and warns |
|
|
406
|
-
| `allOf` composition | the properties of every member are merged into the model (inheritance is flattened, with a warning) |
|
|
407
|
-
|
|
408
|
-
A property that is not in `required` is nullable unless it has a `default` or is `readOnly` (the database fills it in). The conventions of ormbridge's own JSON Schema output are read back: a foreign key scalar (`author_id`) next to an optional `$ref` property (`author`) is one relation whose nullability is the scalar's, reverse sides are arrays that are matched with the foreign keys that point at the model (by `x-related-name`, otherwise by position when the counts agree, with a warning), `readOnly` columns are generated, and `bigInt` and `decimal` columns written as patterned strings (`^-?[0-9]+$`, `^-?[0-9]{1,6}(\.[0-9]{1,2})?$`) are read as `bigInt` and `decimal(8, 2)`. Table names, unique constraints, indexes, composite keys, referential actions, related names, enum labels and the exact owner of a many-to-many field are not JSON Schema keywords, so they only survive through the `x-` conventions above, which the writer produces too (see below).
|
|
409
|
-
|
|
410
|
-
Losses, each reported as a warning naming the model and property: `description` / `title` (the IR has no comments), validation keywords (`minimum`, `pattern`, ...), `oneOf` / `anyOf` unions (a property becomes `json`; a `oneOf` of models is not a model), `patternProperties`, maps (`additionalProperties` with a schema), `if` / `then` / `else`, type unions, tuple arrays, nested inline objects (a `json` column; define them under `$defs` to make them models), arrays of models inside other arrays and non-string enums (the plain type is kept). An array of models with no counterpart on the other model gets a nullable foreign key on that model (JSON Schema cannot say who holds the key). The `x-` keywords above are ormbridge's own conventions and are ignored by other tools. [docs/CONVERSION_MATRIX.md](docs/CONVERSION_MATRIX.md) lists what the reader cannot express.
|
|
411
|
-
|
|
412
|
-
### SQL DDL (`--from sql`)
|
|
413
|
-
|
|
414
|
-
`--from sql` reads the SQL that creates a database: a `pg_dump --schema-only`, `mysqldump --no-data` or SQLite `.dump` script, an SSMS-generated script, hand-written DDL, or a folder of plain `.sql` migrations (read in file-name order, so later `ALTER TABLE` statements apply to earlier tables; `*.down.sql`, `*_rollback.sql`, `undo.sql` and similar rollback scripts are skipped). PostgreSQL, MySQL / MariaDB, SQLite and SQL Server are understood, each file in its own dialect, guessed from characteristic syntax (backticks and `ENGINE=`, `AUTOINCREMENT`, `[brackets]` and `GO`, `serial` and `::` casts) and read as PostgreSQL when nothing decides it; `parseSqlDdl` takes a `dialect` option to force one. `tree-sitter-wasms` has no SQL grammar, so a small static tokenizer and statement parser read the text (comments, every quoting style, string escapes, `$$` dollar quoting, `DELIMITER` and `GO` batches, `COPY ... FROM stdin` data and schema-qualified names); nothing is executed and no database is needed. `.sql` is claimed, so a single file infers the format.
|
|
415
|
-
|
|
416
|
-
| Statement | Read as |
|
|
417
|
-
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
418
|
-
| `CREATE TABLE [IF NOT EXISTS] [schema.]name (...)` | a model named after the table in singular PascalCase (`blog_posts` is `BlogPost`); the table name is kept as written, and a schema other than `public`, `dbo` or `main` is kept as the model schema |
|
|
419
|
-
| column types | `smallint` / `integer` / `tinyint` / `year` are `int`, `bigint` is `bigInt`, `real` / `float` / `double` are `float`, `numeric(p, s)` / `decimal` / `money` (SQL Server) are `decimal` with their digits and places, `varchar(n)` / `char(n)` a string of that length, the `text` family, an unbounded `varchar` and `varchar(max)` are `text`, `boolean` / `bit(1)` / `tinyint(1)` (MySQL) are `boolean`, `timestamp` / `timestamptz` / `datetime` / `datetime2` / `datetimeoffset` are `dateTime`, plus `date`, `time`, `interval`, `uuid` / `uniqueidentifier`, `json` / `jsonb`, `bytea` / `blob` / `varbinary`, `inet`, `hstore`, range types and arrays (`int[]`, `text ARRAY`); SQLite types follow its affinity rules; `UNSIGNED`, display widths and time zones are dropped, and a type with no equivalent (`geometry`, `tsvector`, `xml`, `rowversion`) is an `unsupported` column that keeps its type text |
|
|
420
|
-
| `NOT NULL`, `NULL`, `PRIMARY KEY`, `UNIQUE`, `CONSTRAINT name ...` | nullability, the primary key (several columns make a composite key), unique columns (a constraint name is kept) and unique indexes; a primary key column is never nullable |
|
|
421
|
-
| `DEFAULT` | literals, `true` / `false`, `now()` / `CURRENT_TIMESTAMP` / `getdate()` / `datetime('now')` (the current time), uuid generators (`gen_random_uuid()`, `uuid_generate_v4()`, `NEWID()`, `UUID()`), enum members and `nextval(...)`; any other expression is kept as a database expression |
|
|
422
|
-
| `serial`, `GENERATED ... AS IDENTITY`, `AUTO_INCREMENT`, `AUTOINCREMENT`, `IDENTITY(1,1)`, SQLite `INTEGER PRIMARY KEY` | an auto-incrementing key |
|
|
423
|
-
| `GENERATED ALWAYS AS (...) STORED` / `VIRTUAL`, SQL Server `AS (...) PERSISTED` | a generated column (the expression is kept as written) |
|
|
424
|
-
| MySQL `ON UPDATE CURRENT_TIMESTAMP` | an auto-updated date-time column |
|
|
425
|
-
| `CREATE TYPE ... AS ENUM`, `ALTER TYPE ... ADD VALUE`, MySQL `ENUM('a', 'b')`, a `CHECK (col IN ('a', 'b'))` (or `col = 'a' OR col = 'b'`, or PostgreSQL's `= ANY (ARRAY[...])`) on a text column | an enum (member names are the upper-case snake spelling of the values; a `CREATE TYPE` keeps its database name; an inline or `CHECK` enum is named after the table and column, `ForumThreadStatus`) |
|
|
426
|
-
| `REFERENCES`, `FOREIGN KEY`, `ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY` | a foreign-key relation named after the column without its `_id` suffix (`author_id` is `author`), with `ON DELETE` / `ON UPDATE`, the constraint name, a target column other than the key (`toField`) and a reverse accessor named after the table that holds the key (`blog_posts`; `author_blog_posts` and `editor_blog_posts` when two keys point at the same table). A unique or primary-key foreign key is one-to-one. Several columns make a composite foreign key |
|
|
427
|
-
| a table of exactly two foreign-key columns that together are its primary key, with no other column, index or constraint and no foreign key pointing at it | a many-to-many relation on one side (the table is dropped from the models); the side whose table name starts the join table name owns it (`blog_post_tags` becomes `BlogPost.tags`) |
|
|
428
|
-
| `CREATE [UNIQUE] INDEX`, MySQL `KEY` / `FULLTEXT KEY`, SQL Server `CLUSTERED` | indexes with their names, column order, descending columns, `USING gin` / `hash` / ... methods, operator classes, prefix lengths, `FULLTEXT` and `CLUSTERED` |
|
|
429
|
-
| `ALTER TABLE ... ADD COLUMN`, `ADD CONSTRAINT`, `ADD PRIMARY KEY`, `ALTER COLUMN ... SET / DROP DEFAULT` and `SET / DROP NOT NULL`, SQL Server `ADD DEFAULT ... FOR`; `DROP TABLE`, `DROP TYPE` | applied in order, so a dump that adds keys after the tables (as `pg_dump` does) or a folder of migrations reads as the final schema |
|
|
430
|
-
|
|
431
|
-
Everything else is skipped, and each kind is reported once as a warning with a count and the first line (`Skipped 3 CREATE VIEW statements (first at schema.sql:40)`): views, functions, procedures, triggers, sequences, domains, composite types, row data (`INSERT`, `COPY`), temporary tables, `CREATE TABLE ... AS SELECT`, `PARTITION OF`, and the `ALTER TABLE` changes that are not replayed (`DROP COLUMN`, `RENAME`, a changed column type). `COMMENT ON`, `SET`, `GRANT`, transactions and extensions are skipped without a warning. A `CHECK` that is not an enum, an `EXCLUDE` constraint, an expression index, a partial unique index (a full one would reject rows the source accepts), the `WHERE` of a partial index, `INHERITS` and partitioning, a MySQL `SET` column, a table without a primary key and a foreign key to a table that is not in the input each warn and name the table. MySQL creates an index for every foreign key, so an index over exactly the foreign-key columns is not kept as a separate index.
|
|
432
|
-
|
|
433
|
-
### SQLAlchemy and SQLModel (`--from sqlalchemy`)
|
|
434
|
-
|
|
435
|
-
`--from sqlalchemy` reads SQLAlchemy 2.0 declarative models, the classic `Column(...)` style, `Table(...)` objects and SQLModel classes (the `sqlalchemy` format also covers SQLModel). The Python is read with the same static tree-sitter grammar as Django, so **nothing is imported or run**: no Python, SQLAlchemy or database is needed. `.py` belongs to Django, so pass `--from sqlalchemy`; a directory is walked for `.py` files and skips virtual environments, `site-packages`, `migrations`, `alembic`, `__pycache__`, caches and test or tooling files (`test_*.py`, `*_test.py`, `conftest.py`, `setup.py`).
|
|
436
|
-
|
|
437
|
-
| SQLAlchemy / SQLModel | Read as |
|
|
438
|
-
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
439
|
-
| a class with `__tablename__` on `DeclarativeBase`, `declarative_base()`, `db.Model` or any imported base; a SQLModel class with `table=True`; `__table__` | a model named after the class; the table is `__tablename__` (a `@declared_attr` returning `cls.__name__.lower()` works too; SQLModel and Flask-SQLAlchemy default to the lower-cased and snake-cased class name) |
|
|
440
|
-
| `Mapped[T]`, `mapped_column(...)`, `Column(...)`, SQLModel `Field(...)` | a column; the attribute name is the field name and a name given to `mapped_column("name")` / `Column("name")` is the database column |
|
|
441
|
-
| `Mapped[Optional[T]]`, `Mapped[T \| None]`, `Union[T, None]`, `nullable=` | nullability (a plain `Column` is nullable unless it is a primary key or says `nullable=False`; `nullable=` always wins) |
|
|
442
|
-
| `Integer`, `SmallInteger`, `BigInteger`, `String(n)`, `Text`, `Boolean`, `Float`, `Numeric(p, s)`, `DateTime`, `Date`, `Time`, `Interval`, `LargeBinary` | `int`, `int`, `bigInt`, a string of that length, `text`, `boolean`, `float`, `decimal` with its digits and places, `dateTime`, `date`, `time`, `duration`, `bytes` |
|
|
443
|
-
| `JSON` / `JSONB`, `Uuid` / `UUID`, `ARRAY(T)`, `INET`, `HSTORE`, `INT4RANGE` and friends | `json`, `uuid`, an array of `T` (`dimensions=` adds levels), `ipAddress`, `hstore`, a range; any other type (`Geometry`) is an `unsupported` column with a warning |
|
|
444
|
-
| a bare Python type (`Mapped[int]`, `Mapped[datetime]`, `Mapped[Decimal]`, `Mapped[uuid.UUID]`, SQLModel `str`) | the matching column type; `Annotated[...]` aliases (`intpk = Annotated[int, mapped_column(primary_key=True)]`) and a literal `type_annotation_map` are followed, a `TypeDecorator` is read through its `impl` |
|
|
445
|
-
| `Enum(PyEnum)`, a `Mapped[PyEnum]`, `Enum("a", "b", name="x")`, `Mapped[Literal["a", "b"]]` | an enum; like SQLAlchemy it stores member names unless `values_callable=lambda e: [m.value for m in e]` is given; `name=` becomes the database name of the enum |
|
|
446
|
-
| `primary_key=True` on one or several columns, `PrimaryKeyConstraint(...)` | a primary key or a composite key; a lone integer key counts up (`autoincrement=False` turns that off), as do `Identity()` and `Sequence()` |
|
|
447
|
-
| `default=`, `server_default=`, `onupdate=`, `Computed(...)` | literals, enum members, `func.now()` / `datetime` callables (`now`), `uuid.uuid4`, `dict` / `list` and SQL text (`text("0")`, `text("CURRENT_TIMESTAMP")`); `server_default` marks a database default; `onupdate=func.now()` an auto-updated column; `Computed` a generated column |
|
|
448
|
-
| `ForeignKey("table.col", ondelete=, onupdate=, name=)`, `ForeignKey(Model.col)`, `ForeignKeyConstraint([...], [...])`, SQLModel `foreign_key=` | a foreign-key relation named after the `relationship()` that uses it, otherwise after the column without `_id`; a unique or primary-key foreign key is one-to-one; several columns make a composite foreign key |
|
|
449
|
-
| `relationship(back_populates=, backref=, foreign_keys=, remote_side=, uselist=False)`, SQLModel `Relationship(...)` | the other side of the foreign key becomes `related_name`; `uselist=False` (or a scalar annotation) on the reverse side makes the relation one-to-one; `backref` creates the reverse side; `viewonly` relationships are ignored |
|
|
450
|
-
| `relationship(secondary=Table(...))` (the table as a variable, a name or a `lambda`), SQLModel `link_model=` | one many-to-many on the side declared first (the other side is its related name); the association table is not a model |
|
|
451
|
-
| `Index(...)` in `__table_args__` or after the class, `index=True`, `unique=True`, `UniqueConstraint(...)`, `postgresql_using=` | indexes with their names, column order, descending columns and method; a single unique column becomes a unique field |
|
|
452
|
-
| mixins, `__abstract__ = True` bases, `MappedAsDataclass`, joined-table inheritance | the columns of mixins and abstract bases are copied into each subclass; a joined subclass has a one-to-one primary key to its parent |
|
|
453
|
-
| `Table(...)` that is not an association table | a model named after the class `map_imperatively` maps it to, otherwise after the table; a warning says so |
|
|
454
|
-
| `schema=` in `__table_args__`, `Table(...)` or `MetaData(schema=...)` | the model schema |
|
|
455
|
-
|
|
456
|
-
Not read, each with a warning naming the model and column: `CheckConstraint`, `ExcludeConstraint`, partial indexes (`postgresql_where` and friends), expression indexes (`func.lower(...)`, `text(...)`), single-table inheritance (a subclass without a table of its own), `column_property`, `composite`, `synonym`, `@declared_attr` members other than `__tablename__`, a `Table` reflected with `autoload_with`, a default that is a call the reader does not know, and a Python type with no column type (`dict`, `list[str]` without `JSON` / `ARRAY`). Hybrid properties, `association_proxy`, loader and cascade options and SQLModel's pydantic constraints (`ge`, `min_length`, validators) have no database meaning and are dropped without noise. A foreign key to a table that is not in the input gets a stub model with an auto-increment id and a warning. Hostile or very large input is limited (a file over 2 million characters is skipped, a table keeps its first 2000 columns, at most 500 warnings are printed) and never throws.
|
|
457
|
-
|
|
458
|
-
### Prisma constructs
|
|
459
|
-
|
|
460
|
-
The Prisma parser and emitter cover more than the Django mapping above. Constructs the shared model can carry survive a Prisma → Prisma round trip; the other formats get the closest equivalent and a warning naming the model and field.
|
|
461
|
-
|
|
462
|
-
| Prisma | Read as | Written back / other formats |
|
|
463
|
-
| ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
464
|
-
| `@relation(fields: [a, b], references: [x, y])` (composite foreign key, `onDelete`, `onUpdate`, `map:`) | composite foreign key; the columns stay plain fields | written back with the reverse field; dropped with a warning in Django, TypeORM, TypeScript and Graphene |
|
|
465
|
-
| `@@id([a, b], map: "...")`, `@id(map: "...")`, `@unique(map: "...")`, `@@index(..., map: "...")`, `@relation(map: "...")` | constraint names | written back where the provider supports them (primary key names are dropped with a warning on MySQL, SQLite and MongoDB, foreign key names on SQLite and MongoDB) |
|
|
466
|
-
| `@@index([a(sort: Desc, length: 10, ops: ...)], type: Hash, clustered: ...)` | index options | written back where the provider accepts them (`type` on PostgreSQL, `Gin` also on CockroachDB; `length` on MySQL; `ops` on PostgreSQL; `clustered` on SQL Server), otherwise dropped with a warning |
|
|
467
|
-
| `@@fulltext([a, b])` | full-text index | `@@fulltext` on MySQL and MongoDB; an ordinary `@@index` with a warning elsewhere |
|
|
468
|
-
| `view` blocks | model flagged as a view | `view` block (the header gets `previewFeatures = ["views"]`); other formats treat it as a table and warn |
|
|
469
|
-
| `Unsupported("circle")` | opaque column (`Unsupported("int4range")` and friends become range fields) | `Unsupported("circle")` plus a warning that Prisma Client cannot use it; a plain string column and a warning elsewhere |
|
|
470
|
-
| `@db.*` native types (PostgreSQL, CockroachDB, MySQL, SQL Server, MongoDB) | carried on the field | written back when the target provider accepts that type for the scalar, otherwise dropped with a warning in favour of the provider default |
|
|
471
|
-
| `@default(cuid())`, `ulid()`, `nanoid(n)`, `uuid(7)`, `dbgenerated("...")`, `auto()`, `sequence()` | kept as defaults | written back; dropped with a warning in the other formats (`uuid()` still maps to a UUID default) |
|
|
472
|
-
| `String[]`, `Int[]`, ... (scalar lists), `@default([])` | array fields | scalar list on PostgreSQL, CockroachDB and MongoDB, `Json` elsewhere; `ArrayField` in Django, `array: true` in TypeORM |
|
|
473
|
-
| `@@schema("...")` with the datasource `schemas` list (multiSchema), `@@map` on enums | schema and database name | written back with the `schemas` list on PostgreSQL, CockroachDB and SQL Server, dropped with a warning elsewhere |
|
|
474
|
-
| `@ignore`, `@@ignore` | flags | written back; dropped with a warning in the other formats |
|
|
475
|
-
|
|
476
|
-
The Prisma emitter also adapts to what the target provider rejects, each time with a warning naming the model and field: `Restrict` becomes `NoAction` on SQL Server, enums are written as `String` on SQL Server (which has none), `autoincrement()` on an `Int` becomes `sequence()` on CockroachDB, `Json` is written as `String @db.NVarChar(Max)` on SQL Server, and CockroachDB uses its own type names (`@db.String(n)`, not `@db.VarChar(n)`).
|
|
477
|
-
|
|
478
|
-
Prisma 7 changed the schema header: the `prisma-client` generator replaces `prisma-client-js` and needs an `output` path, and the datasource no longer takes `url` (connection URLs move to `prisma.config.ts`). Programmatic callers can pass `prismaVersion: 7` to `convertText` (or `emitPrisma`) to get that header; the default stays Prisma 6, and there is no CLI flag for it yet. ormbridge does not generate `prisma.config.ts` or the driver adapter that Prisma 7 needs at runtime.
|
|
479
|
-
|
|
480
|
-
### TypeScript interfaces (`--to typescript`)
|
|
481
|
-
|
|
482
|
-
Writes plain TypeScript interfaces and enums for sharing models with a front end (for example an Angular app). It is output only, needs no parser, and `.ts` is not inferred, so pass `--to typescript`.
|
|
483
|
-
|
|
484
|
-
- One `export interface` per model, with properties in the IR's order, and one string `export enum` per enum using the stored values (a Django choice label becomes a `/** ... */` comment). The IR carries no model or field help text, so no other doc comments are written.
|
|
485
|
-
- Types follow what travels as JSON: `string`, `text`, `uuid`, `time`, `bigInt`, `decimal` (to keep precision) and `bytes` (base64) are `string`; `int` and `float` are `number`; `boolean` is `boolean`; `json` is `unknown`. `DateTime` and `Date` are `string` (ISO 8601). Programmatic callers can pass `dates: 'date'` to `emitTypescriptInterfaces` to type them as `Date`; the CLI always writes `string`.
|
|
486
|
-
- Nullable fields get `| null`. Fields are never optional, because the interfaces describe the data a server returns, defaults included.
|
|
487
|
-
- A foreign key is written as its scalar (`categoryId: number`, typed from the referenced primary key or `to_field`) plus an optional expanded relation (`category?: Category`). Reverse sides are optional (`comments?: Comment[]`, a one-to-one reverse side is `profile?: Profile | null`), as are many-to-many lists on both sides.
|
|
488
|
-
- `--naming normalize` writes camelCase property names, `preserve` keeps the existing names. Names that are not valid identifiers are quoted.
|
|
489
|
-
- Warnings name the model and field for relations to unknown models, composite-key targets (the foreign key becomes `unknown`), unknown enums or field types, and invalid or clashing type names.
|
|
490
|
-
|
|
491
|
-
### JSON Schema (`--to json-schema`)
|
|
492
|
-
|
|
493
|
-
Writes one JSON Schema (draft 2020-12) document describing the rows of every model, for validating API payloads or generating forms and clients. It is output only, needs no parser, and `.json` is not claimed as an extension (the registry matches the last extension and `.json` is far too generic), so pass `--to json-schema` and name the file yourself (for example `-o schema.json`).
|
|
494
|
-
|
|
495
|
-
- The document has `$schema`, `$id` (`urn:ormbridge:schema`; programmatic callers can pass `id` to `emitJsonSchema`) and `$defs` with one entry per enum and per model. Every model is `type: "object"` with `additionalProperties: false`. Property names match the TypeScript interfaces output, so the two can be used together: `preserve` keeps names, `normalize` writes camelCase.
|
|
496
|
-
- **Types.** `string` / `text` are `string` (`maxLength` from the column length), `int` is `integer`, `float` is `number`, `boolean` is `boolean`, `dateTime` / `date` / `time` / `uuid` / `duration` are strings with `format` `date-time` / `date` / `time` / `uuid` / `duration` (a name such as `email` never adds `format: email`), `json` accepts any value, `bytes` is a base64 string (`contentEncoding`). `bigInt` and `decimal` are strings so no precision is lost: `bigInt` has the pattern `^-?[0-9]+$`, and `decimal` has `^-?[0-9]+(\.[0-9]+)?$`, narrowed to the allowed digits when the column has a precision and scale (`decimal(8,2)` is `^-?[0-9]{1,6}(\.[0-9]{1,2})?$`). The IR carries no integer sizes, so `minimum` / `maximum` are written only when a Prisma native type names the width (`@db.SmallInt`, `@db.TinyInt`, `@db.UnsignedInt`, ...). Arrays nest `type: "array"`, HStore is an object of nullable strings and ranges are `{ lower, upper, bounds }` objects.
|
|
497
|
-
- **Nullable columns** are `type: ["string", "null"]`. A nullable enum or relation is `anyOf` with `{ "type": "null" }`.
|
|
498
|
-
- **Required.** A property is in `required` when its column is not nullable, has no default and is not generated, and a foreign-key scalar is required when its relation is. Columns the database or ORM fills in (auto-increment, UUID, `now`, client-generated and database-expression defaults, auto-updated and generated columns) are `readOnly: true` and not required; a literal or enum default is written as `default` and makes the column optional. Relation and reverse properties are always optional.
|
|
499
|
-
- **Relations.** A foreign key keeps its scalar (`author_id`, typed from the referenced key) and gets an optional expanded property that is a `$ref` to the target (`anyOf` with null when nullable). Reverse sides and many-to-many relations are optional arrays of `$ref` (a one-to-one reverse side is nullable), exactly as in the TypeScript output.
|
|
500
|
-
- **Enums** are `{ "type": "string", "enum": [stored values] }`. The IR has no column comments, so `description` carries only enum labels (`draft: Draft`) and generated-column expressions.
|
|
501
|
-
- **OpenAPI.** An OpenAPI 3.1 document uses JSON Schema 2020-12, so the entries can be copied into `components.schemas`. References point at `#/$defs/<Name>` by default; pass `refPrefix: '#/components/schemas/'` to `emitJsonSchema` and copy the `$defs` entries over (or rewrite the prefix with a text replace). Generating an OpenAPI document itself (paths, `--to openapi`) is not part of this format.
|
|
502
|
-
- Warnings name the model and field for relations to unknown models, composite-key targets (the foreign key accepts any value), unknown enums or field types, duplicate enum values, and definition names that cannot be used in a `$ref` (they are renamed). Database constraints have no JSON Schema keyword, so they are written as ormbridge's own `x-` keywords, which the JSON Schema reader reads back: `x-table-name` (when the table is not named after the definition), `x-primary-key` (a key that is not called `id`, or a composite key as an array of property names), `x-unique` on a column, `x-indexes` (`{ fields, unique?, name? }` entries), and on a relation property `x-on-delete` (only when it differs from what the reader assumes: `restrict` for a required relation, `set-null` for an optional one) and `x-related-name` (the name of the reverse property). Other tools ignore these keywords, but **Ajv in strict mode rejects unknown keywords**: register them with `ajv.addVocabulary(JSON_SCHEMA_CONVENTION_KEYWORDS)` (exported by the package), or call `emitJsonSchema(schema, { camelFields: false, conventions: false })` for a document without them. Index methods, operator classes, sort orders, full-text kinds, `onUpdate` actions and constraint names have no convention and are not written.
|
|
503
|
-
- The tests check every canonical fixture's output structurally and, when Ajv is installed (`AJV_DIR`, see `test/README.md`), load it with the real draft 2020-12 validator and validate IR-generated sample rows.
|
|
504
|
-
|
|
505
|
-
### Zod schemas (`--to zod`)
|
|
506
|
-
|
|
507
|
-
Writes [Zod](https://zod.dev) 4 schemas and their inferred types, for validating the same data the TypeScript interfaces describe. It is output only, writes one `schemas.ts`-style file (`import { z } from 'zod'` first), and `.ts` is not inferred, so pass `--to zod`. Property names, enum names and the handling of relations follow the TypeScript interfaces emitter, so the two outputs can be used together.
|
|
508
|
-
|
|
509
|
-
- **Per model**: `<Model>Schema` (`z.object({...})`) and `export type <Model> = z.infer<typeof <Model>Schema>`; `<Model>CreateSchema` / `<Model>Create`; `<Model>UpdateSchema` / `<Model>Update`.
|
|
510
|
-
- **Create schema**: leaves out what the database generates (auto-increment keys, generated columns, auto-updated timestamps such as Django `auto_now` and Prisma `@updatedAt`), and makes columns with a default (or a database default) and nullable columns `.optional()`. The **update schema** is `<Model>CreateSchema.partial()`. A view gets neither.
|
|
511
|
-
- **Types**: `string` and `text` are `z.string()` with `.max(n)` from the length; `uuid` is `z.uuid()` (Zod's RFC 9562 check; use `z.guid()` if your data holds other UUID-shaped values); `int` is `z.number().int()`; `float` is `z.number()`; `boolean`; `json` is `z.unknown()`; `bytes` is `z.base64()`; IP addresses are `z.union([z.ipv4(), z.ipv6()])`; times and durations are `z.string()`. `bigInt` is a string of digits (`z.string().regex(/^-?\d+$/)`, like the TypeScript interfaces, because JSON cannot carry a bigint) and `decimal` is a string matching a number pattern sized from `max_digits` and `decimal_places`, to keep precision. Arrays wrap the element in `z.array(...)`, nullable columns get `.nullable()`, hstore is `z.record(z.string(), z.string().nullable())` and a range is an object with nullable `lower` and `upper` plus `bounds`. No `z.email()` or `z.url()` is ever inferred from a name.
|
|
512
|
-
- **Dates** are `z.coerce.date()`: they accept a `Date` or any value `new Date(value)` accepts (ISO text from JSON) and produce a `Date`. Coercion also turns `null` (and numbers) into a date, so a null in a required date column is not rejected; use a nullable column, or pass `dates: 'string'` to `emitZod` for strict ISO text (`z.iso.datetime({ offset: true })` / `z.iso.date()`, which matches the TypeScript interfaces' default). Pass `bigints: 'bigint'` for `z.coerce.bigint()`. The CLI always writes the defaults.
|
|
513
|
-
- **Enums** are a const object with the stored values (`export const Role = { ADMIN: 'admin' } as const;`, so `Role.ADMIN` works like the interface emitter's enum), `export const RoleSchema = z.enum(Role)` and `export type Role = z.infer<typeof RoleSchema>`. Labels become comments.
|
|
514
|
-
- **Relations** are not part of `<Model>Schema`: the foreign-key scalar stays a field (`categoryId`, typed from the referenced key) and relation fields are left out, so a row validates exactly what a table row holds. A model that takes part in a relation also gets `<Model>WithRelationsSchema` (and a hand-written `<Model>WithRelations` type, because mutually recursive schemas cannot be inferred), which extends the row with optional, lazily resolved related rows (`z.lazy`), reverse sides and many-to-many lists included.
|
|
515
|
-
- `--naming normalize` writes camelCase property names, `preserve` keeps the existing names. Warnings name the model and field for relations to unknown models, composite-key targets (the foreign key becomes `z.unknown()`), unknown enums or field types, and invalid or clashing type names.
|
|
516
|
-
- **Zod 3**: the output uses Zod 4 APIs (`z.uuid()`, `z.base64()`, `z.ipv4()`, `z.enum(constObject)`, `z.iso.*`). Zod 3.25 and later ship Zod 4 under `zod/v4`, so change the import to `import { z } from 'zod/v4'`; older Zod 3 releases are not supported.
|
|
517
|
-
|
|
518
|
-
[Back to Table of Contents](#table-of-contents)
|
|
519
|
-
|
|
520
|
-
### SQL DDL (`--to sql`)
|
|
521
|
-
|
|
522
|
-
Writes one `.sql` script of dialect-specific DDL, so the schema can be created in a database directly (or reviewed before a migration is written). It is output only, `.sql` is inferred from the output path, and `--provider` picks the dialect: `postgresql` (also used for `cockroachdb`), `mysql`, `sqlite` or `sqlserver` (`mongodb` has no SQL and falls back to PostgreSQL with a warning). The script holds, in order, the extensions and enum types the dialect needs, one `CREATE TABLE` per model with the tables a table references created first, and the `CREATE [UNIQUE] INDEX` statements of each table. A foreign key that closes a reference cycle is added afterwards with `ALTER TABLE ... ADD CONSTRAINT` (SQLite cannot add a constraint later, but accepts a reference to a table created further down, so its keys stay inline).
|
|
523
|
-
|
|
524
|
-
- **Naming** follows the other emitters: `preserve` keeps the table and column names of the source, `normalize` writes singular snake_case tables with UUID keys, snake_case columns and `created_at` / `updated_at`. Identifiers are bare when they are lower-case words that are not reserved in any of the four dialects, otherwise quoted for the dialect (`"x"`, `` `x` `` or `[x]`, with the quote character doubled); quotes in string literals are doubled, and MySQL also doubles backslashes. Names longer than the dialect allows give a warning, and generated constraint and index names are shortened with a hash.
|
|
525
|
-
- **Types**: `string` is `VARCHAR(n)` (`TEXT` without a length on PostgreSQL and SQLite, `VARCHAR(255)` on MySQL, `NVARCHAR(n)` on SQL Server), `text` is `TEXT` / `LONGTEXT` / `NVARCHAR(MAX)`, `decimal` keeps its precision and scale, `uuid` is `UUID` / `CHAR(36)` / `UNIQUEIDENTIFIER`, `dateTime` is `TIMESTAMPTZ` / `DATETIME(6)` / `DATETIME` / `DATETIME2`, `json` is `JSONB` / `JSON` / `TEXT` with a `json_valid` check / `NVARCHAR(MAX)` with an `ISJSON` check, and booleans are `BOOLEAN` (`BIT` on SQL Server). A Prisma native type (`@db.VarChar`, `@db.Uuid`, `@db.UnsignedInt`, ...) is kept when the dialect has a type of that name and ignored otherwise. Arrays, ranges, hstore and `interval` are PostgreSQL types; the other dialects get JSON or text (an integer of microseconds for durations) with a warning.
|
|
526
|
-
- **Defaults**: literals are quoted and typed for the dialect (booleans are `TRUE` / `FALSE` or `1` / `0`), `now` is `now()`, `CURRENT_TIMESTAMP`, `CURRENT_TIMESTAMP(n)` or `SYSUTCDATETIME()`, and a UUID default is `gen_random_uuid()`, `(UUID())`, `NEWID()` or an expression over `randomblob` on SQLite. Well-known database expressions (`gen_random_uuid()`, `now()`, `uuid_generate_v4()`, `CURRENT_TIMESTAMP`) are translated to the dialect; any other expression is written as it is with a warning. Defaults generated by the ORM client (`cuid()`, `ulid()`) are dropped with a warning. A column the ORM refreshes on update gets `ON UPDATE CURRENT_TIMESTAMP` on MySQL; the other dialects need a trigger, which is not generated, so one warning lists those columns.
|
|
527
|
-
- **Keys**: auto-increment is `GENERATED BY DEFAULT AS IDENTITY`, `AUTO_INCREMENT`, `INTEGER PRIMARY KEY AUTOINCREMENT` or `IDENTITY(1,1)`; primary keys (also composite) are named `CONSTRAINT <table>_pkey`; unique fields are `UNIQUE` constraints and composite unique keys are `CREATE UNIQUE INDEX`; on SQL Server a unique key over a nullable column is a filtered unique index (`WHERE col IS NOT NULL`), so it allows many NULLs like the other databases. Foreign keys are named `<table>_<column>_fkey` and carry `ON DELETE` / `ON UPDATE` (`NO ACTION`, the default, is left out); composite foreign keys, `toField` targets and keys that are themselves relations (multi-table inheritance) are supported. MySQL has no `SET DEFAULT`, and SQL Server rejects cascading actions that form a cycle or a second cascade path: both are written as `NO ACTION` with a warning.
|
|
528
|
-
- **Indexes** carry their sort order, the PostgreSQL access method and operator class, MySQL prefix lengths and `USING`, SQL Server `CLUSTERED` / `NONCLUSTERED`, and full-text indexes (`FULLTEXT` on MySQL, a `gin` index over `to_tsvector` on PostgreSQL, an ordinary index with a warning elsewhere). An index or unique key on a column the dialect cannot index (a `JSON` column on MySQL, `NVARCHAR(MAX)` on SQL Server) is left out with a warning, and one on a MySQL `TEXT` or `BLOB` column covers its first 255 characters.
|
|
529
|
-
- **Enums**: PostgreSQL gets `CREATE TYPE ... AS ENUM` (renamed with an `_enum` suffix when a table has the same name), MySQL an inline `ENUM(...)`, SQLite and SQL Server a text column with a `CHECK (column IN (...))` constraint. Member names and labels are not kept.
|
|
530
|
-
- **Many-to-many** fields become the join tables the other formats create. **Views** have no query in the IR, so they are a commented placeholder (`-- CREATE VIEW ... AS SELECT ... ;`) and a warning.
|
|
531
|
-
- Warnings name the model and field for what the dialect cannot express: relations to unknown models, views or composite keys, unknown enums or field types, generated-column expressions (not SQL), database schemas (`@@schema`, ignored), unreferenceable targets, `SET NULL` on a required relation and every default or index option that was dropped.
|
|
532
|
-
- The tests execute the output: SQLite always (Node's built-in `node:sqlite`, or `better-sqlite3`), PostgreSQL when `SQL_POSTGRES_URL` is set and MySQL or MariaDB when `SQL_MYSQL_URL` is set (see `test/README.md`). After running the script they read the tables, columns, keys and indexes back and compare them with the schema, and insert rows to check defaults, identity, enums and referential actions. SQL Server has no server in the tests; its output is covered by the golden files and unit tests only.
|
|
533
|
-
|
|
534
|
-
[Back to Table of Contents](#table-of-contents)
|
|
535
|
-
|
|
536
|
-
### SQLAlchemy and SQLModel (`--to sqlalchemy`)
|
|
537
|
-
|
|
538
|
-
Writes [SQLAlchemy](https://www.sqlalchemy.org) 2.0 declarative models, or with `--style sqlmodel` [SQLModel](https://sqlmodel.tiangolo.com) classes. It is output only and writes **one `models.py`** (like Django, so `-o` is a file and the output can be printed to stdout); `.py` is inferred as Django, so pass `--to sqlalchemy`. The output needs Python 3.9 or newer and SQLAlchemy 2.0 or newer (SQLModel 0.0.21 or newer for `--style sqlmodel`). `--provider` is ignored: the models do not depend on the database.
|
|
539
|
-
|
|
540
|
-
- **SQLAlchemy style** (default): `class Base(DeclarativeBase)`, one class per model with `__tablename__`, typed `Mapped[...]` attributes built with `mapped_column`, and `relationship(back_populates=...)` on **both** sides of every relation (a forward `Mapped["User"]` / `Mapped[Optional["User"]]` and a reverse `Mapped[list["Post"]]`; the reverse side of a one-to-one is `Mapped[Optional["Profile"]]`). Python `enum.Enum` classes are bound with `Enum(Cls, name=..., values_callable=...)`, so the database stores the enum values, not the member names. Only the imports the module uses are written, forward references are strings, and classes follow the classes their foreign keys point at.
|
|
541
|
-
- **SQLModel style**: `class Post(SQLModel, table=True)` with `Field(...)` and `Relationship(back_populates=...)`. Types SQLModel infers from the annotation (`str`, `int`, `bool`, `date`, `uuid.UUID`, ...) are left to it, `max_length`, `max_digits` and `decimal_places` carry lengths and precision, and `sa_type`, `sa_column_kwargs`, `sa_relationship_kwargs` and (for a foreign key with `onupdate`, `NO ACTION` or a constraint name, which `Field` cannot say) `sa_column` appear only where needed. A column the database fills in is `Optional[...] = Field(default=None, nullable=False, ...)`.
|
|
542
|
-
- **Naming**: attributes are snake_case in both modes, with the database name as the first argument of `mapped_column` (or `sa_column_kwargs={"name": ...}`) when it differs. `preserve` keeps table, column, index and enum type names; `normalize` writes singular snake_case tables, UUID keys, snake_case columns and `created_at` / `updated_at`. An attribute that would shadow Python or SQLAlchemy (`class`, `metadata`, `text`, `datetime`, an enum or model name, and for SQLModel `json`, `copy`, `schema`, ...) gets a trailing underscore, a class that collides with an import gets a `Model` suffix, and enum members that are not identifiers are repaired; each repair is a warning. An unnamed reverse relation is named after the plural of the model (`posts`), a one-to-one after the model, and two relations from one model to the same target get `posts_author` / `posts_editor`.
|
|
543
|
-
- **Types**: `String(n)`, `Text`, `Integer`, `BigInteger`, `Float`, `Numeric(p, s)`, `Boolean`, `DateTime(timezone=True)`, `Date`, `Time`, `Interval`, `Uuid`, `JSON` (`JSON(none_as_null=True)` for a nullable column) and `LargeBinary`. Arrays (`ARRAY`, `dimensions=` for nested ones), `HSTORE`, range types and `INET` are PostgreSQL types; each is written with a `.with_variant(JSON(), "sqlite")` fallback (a `String(45)` for `INET`) so `create_all` also works in a SQLite test database, with a warning. A big auto-increment key is `BigInteger().with_variant(Integer, "sqlite")`.
|
|
544
|
-
- **Defaults**: literal, enum, boolean and `now` defaults are `server_default` (`func.now()`, `true()`, `text(...)`), UUID defaults are `default=uuid.uuid4`, an auto-updated column is `default=func.now(), onupdate=func.now()`, and a Prisma `dbgenerated(...)` expression is `server_default=text(...)`. Client-generated defaults (`cuid()`, `ulid()`) and Prisma default functions are dropped with a warning; a UUID version 7 default is written as `uuid4` with a warning.
|
|
545
|
-
- **Keys**: composite and named primary keys are a `PrimaryKeyConstraint`, composite foreign keys a `ForeignKeyConstraint` (with their relationship), unique and index entries `UniqueConstraint` / `Index` in `__table_args__` (generated names are `ix_<table>_<columns>` / `uq_...`; names the source gives are kept). `ForeignKey(..., ondelete=..., onupdate=...)` carries the referential actions, and the reverse side of a cascading or nulling key sets `passive_deletes=True`. A foreign key takes its column type from the key it references (`toField` targets and keys that are themselves relations are followed). `SET NULL` on a required relation is written as `RESTRICT` with a warning. Two foreign keys between the same tables, and self references, get `foreign_keys=` / `remote_side=`.
|
|
546
|
-
- **Many-to-many** relations are a `Table(...)` with the two foreign keys as a composite primary key (no surrogate `id`, unlike the table Django creates), named `<owner table>_<relation>` and used as `secondary=` on both relationships; a self reference adds `primaryjoin` / `secondaryjoin`. The table is defined before the classes.
|
|
547
|
-
- **Views** are a commented placeholder (SQLAlchemy has no declarative view; reflect it with `Table(..., autoload_with=engine)`) and a warning. A table without a primary key is mapped on its required columns with `__mapper_args__` (SQLModel needs a key, so its first column is marked) and a warning. Database schemas (`@@schema`), generated-column expressions (not SQL), index methods, operator classes, sort order and full-text kinds are dropped with a warning, and a relation to a view or to a model that is not in the input stays a plain column.
|
|
548
|
-
- The tests load the output with real SQLAlchemy 2 and SQLModel (`SQLALCHEMY_PYTHON`, see `test/README.md`): the models are imported, the mappers configured, the PostgreSQL DDL rendered, the tables created in SQLite, and the columns, keys, indexes and relationships compared with the schema; rows are inserted (defaults, enums, foreign keys, many-to-many through the relationships) and the referential actions exercised.
|
|
549
|
-
|
|
550
|
-
[Back to Table of Contents](#table-of-contents)
|
|
551
|
-
|
|
552
|
-
## Limitations and warnings
|
|
553
|
-
|
|
554
|
-
ormbridge never fails silently on something it cannot represent: each case produces a `warning:` on stderr naming the model and field. The main ones:
|
|
555
|
-
|
|
556
|
-
- `settings.AUTH_USER_MODEL` and `get_user_model()` are assumed to be a model named `User`. Models referenced but not found in the input get a stub with an auto-increment id.
|
|
557
|
-
- `GeneratedField` expressions are Python (`F`, `Concat`, ...), so they are carried only into Django output; Prisma and TypeORM get a regular column and a warning. Range fields are `Unsupported(...)` in Prisma and `string` in TypeORM and TypeScript objects (`{ lower, upper, bounds }`), nested arrays are `Json` in Prisma, `size=` on `ArrayField` is not kept, and `FileField` / `ImageField` / `FilePathField` / `GenericIPAddressField(protocol=...)` options are not kept (they come back as `CharField` or a plain IP field). `UniqueConstraint(condition=...)` (partial unique constraints), `GenericForeignKey` / `GenericRelation` and `CheckConstraint` are skipped with a warning.
|
|
558
|
-
- Python is parsed statically, so computed defaults, custom field classes, and fields added dynamically are skipped or approximated.
|
|
559
|
-
- Prisma composite primary keys become `models.CompositePrimaryKey` (Django 5.2+). Composite foreign keys, `Unsupported(...)` types, views, `@@fulltext`, `@@schema`, `@ignore` and client-generated defaults (`cuid()`, `ulid()`, `nanoid()`) have no direct Django, TypeORM, TypeScript or Graphene equivalent (except that TypeORM writes a view as a `@ViewEntity`, see below); the composite key columns stay as plain fields, a view becomes a table in Django, TypeScript and Graphene, and each case warns. Scalar lists become `ArrayField` (PostgreSQL only).
|
|
560
|
-
- Not read from Prisma schemas: composite types (`type` blocks, MongoDB), `relationMode`, the client-side `name:` of `@@unique` / `@@id`, partial indexes (`where:`), `@default(..., map:)` constraint names (SQL Server), and sort / length options on `@@id`. Native types are matched to the target provider by name only (the table was built with `prisma validate`); arguments such as a `VarChar` length are passed through unchanged. MongoDB output warns about ids that are not `@map("_id") @db.ObjectId` and about `Decimal` and compound ids, which Prisma rejects there. SQL Server cannot index `NVarChar(Max)` and MySQL needs a prefix length to index `TEXT`; indexes on such columns warn.
|
|
561
|
-
- Prisma's implicit many-to-many join table (`_AToB`) differs from Django's, so data needs migrating.
|
|
562
|
-
- TypeORM: entities are read from decorators only. Not supported, each with a warning naming the entity and column: `EntitySchema`, `@ChildEntity` / `@TableInheritance` (single-table inheritance), `@Tree*`, `@ObjectIdColumn`, `@VirtualColumn`, array columns, partial and spatial indexes, `@Check` / `@Exclusion`, composite foreign keys, relations inside embedded entities, and column options such as `unsigned`, `collation` and `transformer`. `@ViewEntity` classes are read as views (`isView`) with their `@ViewColumn` columns; the view's `expression` is not converted. A Prisma view is written as `@ViewEntity({ name, expression })` with `@ViewColumn()` columns and a placeholder `expression` (`SELECT * FROM <view>`, which selects from the view itself) plus a TODO comment and a warning: replace it with the query that defines the view, because a Prisma schema carries no view definition. Primary key, unique and index information on a view is dropped. Custom `@JoinTable` names are not preserved. Relations without `onDelete` become `NoAction` and relations are nullable unless `nullable: false`, as in TypeORM. Numeric enums become integers.
|
|
563
|
-
- TypeORM many-to-many relations use TypeORM's join table (composite key), so Django's surrogate `id` column on the join table is not reproduced. SQL Server has no enum column, so enums become `varchar`. Database-side UUID defaults are not available on SQLite. MongoDB entities are not supported.
|
|
564
|
-
- Doctrine: only PHP 8 attributes are read. Docblock annotations (`@ORM\Entity`) are detected and reported with a warning (or an error when nothing else is found), and XML / YAML mappings are not read; both are planned. Lifecycle callbacks other than a plain timestamp assignment, and constructor or initializer values that need to run PHP (method calls, constants other than enum cases), are ignored with a warning. Not supported, each with a warning naming the entity and property: attribute and association overrides, composite foreign keys (several `#[ORM\JoinColumn]`), `columnDefinition`, `unsigned` / `comment` / `collation` and other column `options`, `insertable: false` / `updatable: false`, partial and spatial indexes, table `schema` and `options`, custom id generators and `#[ORM\SequenceGenerator]`, `array` / `object` / `simple_array` / custom DBAL types, int-backed and non-backed enums, relations inside embeddables, and the `#[ORM\DiscriminatorMap]` values (the discriminator column is kept, the map is not). Nullability follows the attribute (`nullable: true`), never the PHP type: Doctrine does not infer it, so `?string` with a plain `#[ORM\Column]` is `NOT NULL`. Custom join table names, ORM-level `cascade` / `fetch` / `orphanRemoval` and other runtime-only settings are not part of the schema and are not converted; `cascade: ['remove']` and `orphanRemoval` warn. A custom naming strategy configured outside the attributes cannot be seen, so Doctrine's underscore naming is assumed. Classes the input does not contain (a vendor base class or trait, an enum in another package) produce a warning that names the missing class.
|
|
565
|
-
- Laravel: the schema is rebuilt statically, so what Laravel decides at run time is not seen. A conditional or loop inside a migration is applied as if it always ran, and a dynamic table or column name is skipped; both warn and name the table. Raw SQL (`DB::statement`, `DB::unprepared`) is not interpreted, so a view, trigger, partial index or column created that way is missing, with a warning naming the table. Not represented, each with a warning naming the table and column: polymorphic relations (`morphs`, `morphTo`, `morphMany`, `morphToMany`; the `_type` and `_id` columns stay plain columns), derived relations (`hasManyThrough`, `hasOneThrough`, `*OfMany`), relation constraints (`->where(...)`), composite foreign keys, foreign keys to tables no migration creates, tables that migrations alter but never create (for example `users` from a package), `set`, geometry and `vector` columns, `macAddress` (kept as a string), identity columns, spatial indexes, custom pivot table and key names (the join table gets the name other formats derive: `blog_post_tags` survives, `post_tag` does not), pivot timestamps and non-cascading pivot keys, and `parentKey` / `relatedKey` that are not the primary key. Casts to custom cast classes and to int-backed enums are ignored. Mass-assignment and visibility properties (`$fillable`, `$guarded`, `$hidden`) are not part of the schema. Soft deletes have no IR equivalent beyond the nullable `deleted_at` column.
|
|
566
|
-
- GORM: only struct tags are read and nothing is executed, so a `TableName()` that computes its result, a custom `NamingStrategy` (prefix, `SingularTable`, `NoLowerCase`) set when the database is opened, and `Scanner` / `Valuer` types are not seen: the default naming is assumed, and such a type is an unsupported column unless a `type:` tag names the column type. Not represented, each with a warning naming the struct and field: polymorphic associations (`polymorphic:`; the `<Name>ID` and `<Name>Type` columns stay plain fields), `check:` constraints, column `comment:`, partial (`where:`) and expression indexes, soft delete beyond the nullable `deleted_at` column, integer-backed constant types (a plain integer column), `autoCreateTime` / `autoUpdateTime` on integer fields (Unix time), custom join table and column names, and `serializer:` values other than `json`, `gob` and `unixtime`. GORM ignores a `constraint:` tag on a belongs-to field when the other side describes the same key, and so does the parser (with a warning). Slices and maps without a `type:` or `serializer:` tag, which GORM cannot migrate, are read as arrays and `json` with a warning. Unsigned integers need one more bit in the type mapping, as in GORM's PostgreSQL dialect (`uint32` is `bigInt`).
|
|
567
|
-
- Drizzle: only the TypeScript source is read and nothing is executed, so tables built in loops or by functions, column helpers that need run-time values and defaults computed at run time are not seen (each unresolvable table, column, spread, reference or default warns and names the file and table). Not represented, each with a warning: views, sequences, roles and policies, `check()` constraints, partial (`.where()`) indexes (a partial unique index is dropped because a full one would reject rows the source accepts; a partial plain index is read as a full one), expression indexes, index access methods Prisma does not have, `nullsNotDistinct`, `unsigned`, and relations v2 (`defineRelations`; the foreign keys are still read). SQLite `integer({ mode: 'timestamp' })` is read as a date-time column and warns that the database stores Unix time. A SQLite `integer().primaryKey()` is read as written: it is not auto-increment unless `{ autoIncrement: true }` is given, although SQLite assigns row ids anyway. Drizzle has no implicit many-to-many, so the explicit join table is a model and is written as one. Tables referenced but not found in the input get a stub with an auto-increment id. Drizzle can only be read for now; a writer is planned.
|
|
568
|
-
- JSON Schema: only JSON is read (no YAML), remote `$ref`s are never fetched and named anchors (`$anchor`, `$dynamicRef`) are not resolved. Relations are inferred from `$ref` properties and the `x-` conventions above, and a few guesses are warned about (a one-to-one pair with no required side, a lone array of models, a missing primary key). See "JSON Schema and OpenAPI" above and the conversion matrix for the full list of what is lost.
|
|
569
|
-
- SQL DDL: only the DDL text is read and nothing is run. The dialect is guessed per file, `UNSIGNED`, display widths, time zones and `jsonb` versus `json` are not kept, and names the database generated for constraints are not recorded. Relation names, reverse accessors, enum names and the many-to-many relation name are derived from table and column names, so they are guesses (the conversion matrix lists them). `ALTER TABLE` is replayed for added columns and constraints and a few column options only; renames, dropped columns and type changes are skipped with a warning, so a long migration history is best read from a schema dump. A composite foreign key that points at columns that are themselves single-column foreign keys cannot be expressed, and the Prisma writer reports it. Input is limited to 32 million characters; deeply nested or unterminated constructs are scanned in linear time and never overflow the stack.
|
|
570
|
-
- SQLAlchemy and SQLModel: the Python is read statically and nothing is imported, so models built in loops or functions, computed table names, `__table_args__` produced by a `@declared_attr` and column types chosen at run time are not seen. Not represented, each with a warning: check and exclude constraints, partial and expression indexes, single-table inheritance, `column_property` / `composite` / `synonym`, and Python types that need an explicit SQLAlchemy type (`dict`, `list`). Which side of a `relationship()` owns the foreign key is worked out from `foreign_keys`, `remote_side`, `primaryjoin` or the only key between the two tables; an ambiguous pair warns. A many-to-many keeps its two names but not the name or `ondelete` of its association table (see [docs/CONVERSION_MATRIX.md](docs/CONVERSION_MATRIX.md)).
|
|
571
|
-
- SQLAlchemy and SQLModel output: attributes are snake_case (the column name is passed explicitly), a reverse side that the source never named gets the plural of the model name, and the association table of a many-to-many relation has no surrogate `id` (Django's own does, so data needs migrating). Array, HSTORE, range and `INET` columns are PostgreSQL types with a JSON fallback for SQLite only, other databases are not covered, and a `String` without a length cannot be created on MySQL. Views, database schemas, generated-column expressions, index methods and operator classes are not expressed (each warns). SQLModel's `Field` cannot declare an `onupdate` or a named foreign key, so those columns use `sa_column`. The output is not run through Black or Ruff.
|
|
572
|
-
- Generated names for Django indexes are shortened to Django's 30-character limit.
|
|
573
|
-
|
|
574
|
-
Always review the output and run your own migrations/`prisma validate` before applying it to a real database.
|
|
575
|
-
|
|
576
|
-
### Verified against real tools
|
|
83
|
+
Prisma to Zod schemas:
|
|
577
84
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
[docs/CONVERSION_MATRIX.md](./docs/CONVERSION_MATRIX.md) shows, for every pair of readable and writable formats (Django, Prisma, TypeORM, Doctrine, Laravel, GORM, Drizzle, JSON Schema, SQL, SQLAlchemy), whether the schema survives a round trip and exactly what is lost when it does not (for example enum labels and column lengths, and Prisma's explicit join model for many-to-many fields). Write-only targets (GORM, Drizzle, JSON Schema and Zod) get a structural check of the written files instead of an IR comparison. It is generated from the test fixtures with `npm run docs:matrix`, and the tests fail if it is out of date.
|
|
85
|
+
```bash
|
|
86
|
+
ormbridge convert -i ./prisma/schema.prisma --to zod -o ./src/schemas.ts
|
|
87
|
+
```
|
|
583
88
|
|
|
584
|
-
|
|
89
|
+
Fail a build when generated files are out of date:
|
|
585
90
|
|
|
586
|
-
|
|
91
|
+
```bash
|
|
92
|
+
npx ormbridge convert --check
|
|
93
|
+
```
|
|
587
94
|
|
|
588
|
-
|
|
95
|
+
Use it from code:
|
|
589
96
|
|
|
590
97
|
```ts
|
|
591
98
|
import { convertText } from 'ormbridge';
|
|
592
99
|
|
|
593
|
-
const result = await convertText(
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
} else {
|
|
100
|
+
const result = await convertText(
|
|
101
|
+
[{ path: 'schema.prisma', text: prismaSource }],
|
|
102
|
+
{
|
|
103
|
+
from: 'prisma',
|
|
104
|
+
to: 'django',
|
|
105
|
+
naming: 'preserve',
|
|
106
|
+
provider: 'postgresql',
|
|
107
|
+
header: true,
|
|
108
|
+
autoField: 'int',
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
if (result.ok) {
|
|
606
112
|
console.log(result.value.output);
|
|
607
|
-
console.warn(result.value.warnings);
|
|
608
113
|
}
|
|
609
114
|
```
|
|
610
115
|
|
|
611
|
-
|
|
116
|
+
The [API reference](./docs/API.md) lists every function. Each folder in [`examples/`](./examples/README.md) is a project you can run.
|
|
612
117
|
|
|
613
|
-
|
|
118
|
+
[Back to Top](#table-of-contents)
|
|
614
119
|
|
|
615
|
-
|
|
120
|
+
## More Information
|
|
616
121
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
emit: (schema, options) => ({ ok: true, value: { text: '', warnings: [] } }),
|
|
627
|
-
});
|
|
628
|
-
```
|
|
122
|
+
- [Command line reference](./docs/CLI.md): options, config file, exit codes, multi-file output, and naming modes.
|
|
123
|
+
- [Format guides](./docs/formats/README.md): what each format reads and writes, and what it loses.
|
|
124
|
+
- [Examples](./examples/README.md): one runnable project per format.
|
|
125
|
+
- [Field mapping](./docs/FIELD_MAPPING.md): how each field type converts.
|
|
126
|
+
- [Limitations and warnings](./docs/LIMITATIONS.md): what cannot be converted, and how it was tested.
|
|
127
|
+
- [Conversion matrix](./docs/CONVERSION_MATRIX.md): what survives a round trip between every pair of formats.
|
|
128
|
+
- [API reference](./docs/API.md): `convertText`, the format registry, and more.
|
|
129
|
+
- [Changelog](./CHANGELOG.md) and [roadmap](./ROADMAP.md).
|
|
130
|
+
- [Test guide](./test/README.md): how the tests and the optional real-tool checks work.
|
|
629
131
|
|
|
630
|
-
|
|
132
|
+
[Back to Top](#table-of-contents)
|
|
631
133
|
|
|
632
|
-
|
|
134
|
+
## Buy Me a Coffee
|
|
633
135
|
|
|
634
|
-
|
|
635
|
-
emit: (schema) => ({
|
|
636
|
-
ok: true,
|
|
637
|
-
value: {
|
|
638
|
-
files: { 'src/Entity/Post.php': '<?php ...', 'config/orm.yaml': '...' },
|
|
639
|
-
warnings: [],
|
|
640
|
-
},
|
|
641
|
-
}),
|
|
642
|
-
```
|
|
136
|
+
If this app, code, or repository has helped you or someone you know, please consider donating. I appreciate any help to offset the costs of development and/or AI Credits.
|
|
643
137
|
|
|
644
|
-
|
|
138
|
+
[**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
|
|
139
|
+
|
|
140
|
+
[](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
|
|
645
141
|
|
|
646
|
-
[Back to
|
|
142
|
+
[Back to Top](#table-of-contents)
|
|
647
143
|
|
|
648
144
|
## License
|
|
649
145
|
|
|
650
146
|
MIT
|
|
651
147
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
(c)2026 Richard McQuiston.
|
|
148
|
+
[Back to Top](#table-of-contents)
|
|
655
149
|
|
|
656
|
-
##
|
|
150
|
+
## Copyright
|
|
657
151
|
|
|
658
|
-
|
|
152
|
+
(c)2026 Richard McQuiston. All rights reserved.
|
|
659
153
|
|
|
660
|
-
[
|
|
661
|
-
|
|
662
|
-
[](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
|
|
154
|
+
[Back to Top](#table-of-contents)
|