prorm models pull reads a live database and writes the model files thatDataTypes.X(...) call that recreates it, with its length, precision, timesrc/codegen, so a build script can read aSchemaModel, makes everyts-prorm-orm/codegen and can be used onmodel:create is an alias, and still accepts its original --out-dir,--table and --storage flags..env needs no connection flags:DATABASE_URL=postgres://app:secret@localhost:5432/app$ prorm models pull --schema public --out src/models| Flag | Default | Meaning |
|---|---|---|
--url, --dialect, --database, --host, --port, --username, --password | $DATABASE_URL / DB_* | The connection, as every local command takes it. --dialect accepts every SQL dialect. |
--out (--out-dir) | ./models | Directory to write to. |
--tables (--table) | every base table | Comma-separated table names. A name that does not exist is reported and skipped. |
--schema | the connection's current one | PostgreSQL schema, MySQL database, SQL Server schema, Oracle owner, BigQuery dataset, Snowflake schema, … |
--style | define | define: an init<Model>(prorm) per file calling prorm.define(). class: @Table classes. |
--lang | ts | ts or js. The class style is TypeScript only. |
--module | cjs | cjs or esm, for --lang js. |
--naming | preserve | preserve keeps column names as attribute names; camel camelCases them with a field: mapping (see Naming). |
--no-timestamps | on | Don't turn createdAt/updatedAt/deletedAt columns into prorm-managed timestamps. |
--no-associations | on | Don't generate associations from foreign keys. |
--dry-run | Print the files; write nothing. With --json, the files are in the JSON. | |
--force | Overwrite existing files. Without it nothing is written if any target file exists. |
0 done, 1 refused to overwrite / generation failed, 2 usage5 every requested table was missing.$ prorm models pull --dialect sqlite --database ./shop.sqlite --out src/models
✓ Generated 4 model(s) from sqlite (catalog: sqlite):
src/models/Authors.ts
src/models/BookTags.ts
src/models/Books.ts
src/models/Tags.ts
src/models/index.tssrc/
models/
Authors.ts # timestamps: true, createdAt: 'created_at'
BookTags.ts # the join table
Books.ts # paranoid: true, the CHECK read back as an ENUM
Tags.ts
index.ts # initModels(): every model plus the associationsindex.ts it wrote wires every association the foreign keys imply,book_tags:export function initModels(prorm: Prorm) {
const Authors = initAuthors(prorm);
const BookTags = initBookTags(prorm);
const Books = initBooks(prorm);
const Tags = initTags(prorm);
BookTags.belongsTo(Tags, { foreignKey: 'tag_id', as: 'tag' });
BookTags.belongsTo(Books, { foreignKey: 'book_id', as: 'book' });
Books.belongsTo(Authors, { foreignKey: 'author_id', as: 'author', onDelete: 'CASCADE' });
Authors.hasMany(Books, { foreignKey: 'author_id', as: 'books', onDelete: 'CASCADE' });
Tags.belongsToMany(Books, { through: { model: BookTags, otherKey: 'book_id' }, foreignKey: 'tag_id', as: 'books' });
Books.belongsToMany(Tags, { through: { model: BookTags, otherKey: 'tag_id' }, foreignKey: 'book_id', as: 'tags' });
return { Authors, BookTags, Books, Tags };
}status TEXT CHECK (status IN (…)) comes back asDataTypes.ENUM('draft', 'published'), typed 'draft' | 'published' inBooksAttributes.import { Prorm } from 'ts-prorm-orm';
import { initModels } from './models';
export const prorm = new Prorm({ dialect: 'sqlite', storage: './shop.sqlite' });
export const models = initModels(prorm); // before connect(): see "Using the models on an empty database"
await prorm.connect();
const { Authors, Books } = models;
const author = await Authors.findOne({ where: { email: 'ada@example.com' } });
const books = await Books.findAll({ where: { author_id: author?.get('id') } });$ prorm models pull --dialect sqlite --database ./shop.sqlite --out src/models
✗ Refusing to overwrite 5 existing file(s) (pass --force to overwrite):
/app/src/models/Authors.ts
/app/src/models/BookTags.ts
/app/src/models/Books.ts
/app/src/models/Tags.ts
/app/src/models/index.ts
$ echo $?
1ALTER TABLE authors ADD COLUMN bio TEXT andADD COLUMN verified BOOLEAN NOT NULL DEFAULT 0, regenerating that one table$ prorm models pull --dialect sqlite --database ./shop.sqlite --tables authors --out src/models --force
! authors.verified: kept native type BOOLEAN (DataTypes.BOOLEAN() would create INTEGER on sqlite).
✓ Generated 1 model(s) from sqlite (catalog: sqlite):
src/models/Authors.ts
src/models/index.tsAuthors.ts changes by exactly the new columns: email: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
},
+ bio: {
+ type: DataTypes.TEXT(),
+ },
+ verified: {
+ type: 'boolean', // native: BOOLEAN — DataTypes.BOOLEAN() would create INTEGER on sqlite
+ allowNull: false,
+ defaultValue: false,
+ },
},[!WARNING] --tablesrewritesindex.tstoo
Theindex.tswritten with--tables authorsregisters onlyAuthors: the
other models and every association disappear from it. Either re-pull the
whole schema with--forceand review the diff in git, or regenerate one
table with--dry-runand paste the part you want.--forcealso discards
any hand edits in the files it rewrites.
5:$ prorm models pull --dialect sqlite --database ./shop.sqlite --tables nope --dry-run
! Table 'nope' does not exist, skipping.
! Table 'nope' does not exist.
! No tables found; nothing to generate.
$ echo $?
5--style define (the default), one file per table plus an index:// src/models/Books.ts
import { DataTypes } from 'ts-prorm-orm';
import type { Prorm } from 'ts-prorm-orm';
export interface BooksAttributes {
id: number;
author_id: number;
title: string;
price?: number | string | null;
deleted_at?: Date | null;
}
export let Books: ReturnType<Prorm['define']>;
export function initBooks(prorm: Prorm): ReturnType<Prorm['define']> {
Books = prorm.define<BooksAttributes>(
'Books',
{
id: { type: DataTypes.INTEGER(), primaryKey: true },
author_id: { type: DataTypes.INTEGER(), allowNull: false },
title: { type: DataTypes.STRING(200), allowNull: false },
price: { type: DataTypes.DECIMAL(12, 4), defaultValue: 1.5, comment: 'Net, in EUR' },
},
{
tableName: 'books',
timestamps: false,
paranoid: true,
deletedAt: 'deleted_at',
uniqueKeys: [{ fields: ['author_id', 'title'] }],
indexes: [{ name: 'ix_books_title', fields: ['title'] }],
},
);
return Books;
}// src/models/index.ts
export { Authors, initAuthors } from './Authors';
export { Books, initBooks } from './Books';
export function initModels(prorm: Prorm) {
const Authors = initAuthors(prorm);
const Books = initBooks(prorm);
Books.belongsTo(Authors, { foreignKey: 'author_id', as: 'author', onDelete: 'CASCADE' });
Authors.hasMany(Books, { foreignKey: 'author_id', as: 'books', onDelete: 'CASCADE' });
return { Authors, Books };
}initModels(prorm) once after creating the instance. For --style class@Table, @Attribute, @PrimaryKey,@BelongsTo(() => Authors, …), …) and the index exportsregisterModels(prorm), which calls prorm.addModel() for each.tags table in both styles, as --dry-run prints them:export interface TagsAttributes {
id?: number;
label: string;
}
export let Tags: ReturnType<Prorm['define']>;
export function initTags(prorm: Prorm): ReturnType<Prorm['define']> {
Tags = prorm.define<TagsAttributes>(
'Tags',
{
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
label: { type: DataTypes.STRING(40), allowNull: false, unique: true },
},
{ tableName: 'tags', timestamps: false },
);
return Tags;
}import { Model, DataTypes, Table, Attribute, PrimaryKey, AutoIncrement, NotNull, Unique } from 'ts-prorm-orm';
import type { CreationOptional, InferAttributes, InferCreationAttributes } from 'ts-prorm-orm';
@Table({
tableName: 'tags',
timestamps: false,
})
export class Tags extends Model<InferAttributes<Tags>, InferCreationAttributes<Tags>> {
@Attribute(DataTypes.INTEGER())
@PrimaryKey()
@AutoIncrement()
declare id: CreationOptional<number>;
@Attribute(DataTypes.STRING(40))
@NotNull()
@Unique()
declare label: string;
}mapNativeType),normalizeDataType gives aDataTypes value. The generator writes the factory call for it:| Native (examples) | Generated |
|---|---|
character varying(100), VARCHAR2(40), nvarchar(100) | DataTypes.STRING(100) |
int unsigned, bigint, HUGEINT, tinyint | DataTypes.INTEGER({ unsigned: true }), DataTypes.BIGINT(), DataTypes.HUGEINT(), DataTypes.TINYINT() |
numeric(12,4), NUMBER(12,2) | DataTypes.DECIMAL(12, 4) |
timestamp(3) with time zone, datetimeoffset(3), TIMESTAMP_TZ(9) | DataTypes.TIMESTAMPTZ(3) |
TIMESTAMP WITH LOCAL TIME ZONE, TIMESTAMP_LTZ | DataTypes.TIMESTAMP({ timezone: 'local' }) |
enum('a','b'), a PostgreSQL enum type, SQLite TEXT CHECK (x IN (…)) | DataTypes.ENUM('a', 'b') (named: .setName('mood')) |
integer[], INTEGER[3], ARRAY<STRING> | DataTypes.ARRAY(DataTypes.INTEGER()), …, { size: 3 } |
STRUCT(x INT, y VARCHAR), MAP(VARCHAR, INT) | DataTypes.STRUCT({ x: …, y: … }), DataTypes.MAP(…, …) |
geometry(Point,4326) | DataTypes.GEOMETRY('POINT', 4326) |
vector(1536), halfvec(768) | DataTypes.VECTOR(1536), DataTypes.VECTOR(768, { element: 'float16' }) |
STRING. When there is no portablexid, a domain, an unconstrained numeric (whichDataTypes.DECIMAL() would pin to (10,0)) — it writes the native type as theconst attributes = {
xmin_copy: {
type: 'xid', // native: xid — no portable DataTypes equivalent
},
};getDataTypeSql() and mapped back. If theBIT), or would create aINTEGER is 64-bit; under the defaulttypeMapping: 'legacy' some types are created with their 4.8 spelling, e.g. aVARCHAR(255)), the native type is kept so that sync()new Prorm({ …, typeMapping: 'modern' }) to generate portable types for'x'::text,((0)), N'x', MySQL's bare values, CAST('t' AS BOOLEAN)) into a'0' is 0 on an INTEGER and '0' on aCURRENT_TIMESTAMP, gen_random_uuid()) is kept asprorm.literal('…') in the define style,Prorm.prototype.literal('…') in the class style. Sequence and identityautoIncrement: true.primaryKey: true; a composite key becomesconstraints: [{ type: 'PRIMARY KEY', fields: [...] }] in the define style.unique: true (or theuniqueKeys (define) or uniqueindexes (class, whose @Table has no uniqueKeys).indexes: [{ name, fields, unique, using, where }].check is deprecated in model options).comment:.Source.belongsTo(Target, { foreignKey, as, targetKey?, onDelete?, onUpdate? }),author_id → author);hasOne when the key is unique, else hasMany,books), with As<Alias> appended when twobooksAsAuthor, booksAsEditor) orbelongsToMany on both ends, through the join model:Books.belongsToMany(Tags, {
through: { model: BookTags, otherKey: 'tag_id' },
foreignKey: 'book_id',
as: 'tags',
});createdAt/created_at and updatedAt/updated_at (any case,timestamps: true, with createdAt: /updatedAt: naming the columns when they are not the defaults; a nullabledeletedAt/deleted_at turns on paranoid: true. Those columns are thendeclares them for typing). Pass --no-timestamps to keep them as plainblog_post → BlogPost),Date,Object) or an imported name (Model, DataTypes) gets a Model suffix; a_; names that differ only in case get a number.--naming preserve); any'first name': {…}).--naming camel writes camelCase attributes with field: 'column_name'@Field('column_name') in the class style). A column whose name wouldsave, update, destroy, …) becomes<name>Value with a field mapping in either mode. prorm reads and writesdefine() creates a model's table as soon as it runs on a connected instanceinitModels(prorm) before connect(), or run await prorm.sync({ force: true })sync() is a no-op.table.known) and in the file| Dialects | Catalog | Coverage |
|---|---|---|
| SQLite, Turso, D1, rqlite | sqlite_master, PRAGMA table_info / index_list / index_info / foreign_key_list | Full. CHECKs are read from the CREATE TABLE text; SQLite has no comments. |
| PostgreSQL, TimescaleDB, Citus, YugabyteDB, Greenplum, openGauss, AlloyDB, Neon, Supabase, CockroachDB, Yellowbrick, ParadeDB, the managed PostgreSQL services | pg_catalog (format_type, pg_enum, pg_constraint, pg_index, obj/col_description) | Full, including enum types, identity columns, partial and method-specific indexes. Parts an engine's pg_catalog lacks fall back to information_schema or are reported unknown (Materialize, RisingWave, Aurora DSQL). |
| MySQL, MariaDB, TiDB, SingleStore, OceanBase, PolarDB, Vitess, Dolt, HeatWave, Doris, StarRocks, the managed MySQL/MariaDB services | MySQL information_schema | Full. CHECKs need MySQL 8.0.16+ / MariaDB 10.2+ (unknown before). MariaDB JSON (LONGTEXT + json_valid) is read as JSON. |
| SQL Server, Azure SQL / MI, Fabric, Synapse, RDS and Cloud SQL for SQL Server | INFORMATION_SCHEMA.COLUMNS, sys.indexes, sys.foreign_keys, sys.check_constraints, MS_Description | Full, including identity columns and filtered indexes. |
| Oracle, Autonomous Database, RDS for Oracle | ALL_TAB_COLUMNS, ALL_CONSTRAINTS, ALL_INDEXES/ALL_IND_EXPRESSIONS, ALL_*_COMMENTS | Full (identity columns on 12c+). Names keep their dictionary case. |
| Db2, RDS for Db2 | SYSCAT | Full. |
| SAP HANA | SYS.TABLES, SYS.CONSTRAINTS, SYS.REFERENTIAL_CONSTRAINTS, SYS.INDEX_COLUMNS | Full. |
| Firebird | RDB$ system tables (columns included) | Full. |
| DuckDB, MotherDuck | duckdb_tables/columns/constraints/indexes() | Full, including named ENUMs, fixed arrays, STRUCT/MAP/UNION. |
| Vertica | v_catalog | Keys, foreign keys, CHECKs, comments. No indexes (projections). |
| Exasol | EXA_ALL_* | Primary and foreign keys, comments. The engine has no UNIQUE, CHECK or indexes. |
| Teradata | DBC.*V | Primary key, UNIQUE, secondary indexes, foreign keys, comments. CHECKs unknown. |
| BigQuery | dataset INFORMATION_SCHEMA | Columns (nested STRUCT/ARRAY), descriptions, the unenforced primary and foreign keys. The engine has no UNIQUE, CHECK or secondary indexes. |
| Snowflake | DESCRIBE TABLE, SHOW PRIMARY/UNIQUE/IMPORTED KEYS, SHOW INDEXES | Columns, identity, comments, the informational keys, hybrid-table indexes. No CHECKs (engine). |
| Databricks | Unity Catalog information_schema | Keys, foreign keys, CHECKs, comments. No indexes (engine). |
| ClickHouse | system.columns / tables / data_skipping_indices | Columns, comments, primary key, skipping indexes. No foreign keys or UNIQUE (engine); CHECKs unknown. |
| Redshift, Redshift Data API | information_schema | Keys and foreign keys. No CHECKs or indexes (engine); comments unknown. |
| Spanner, Spanner PostgreSQL | INFORMATION_SCHEMA (+ INDEXES) | Keys, foreign keys, CHECKs, indexes. Comments unknown. |
| Trino, Presto, Athena | information_schema | Columns and column comments. Keys and CHECKs depend on the connector: unknown. |
| CrateDB | information_schema | Primary key, CHECKs. No foreign keys or indexes (engine). |
| MonetDB | information_schema | Keys, foreign keys, CHECKs. Indexes unknown. |
| QuestDB | describeTable(), table_columns() | Columns and symbol indexes. No keys (engine). |
| Druid | INFORMATION_SCHEMA.COLUMNS | Columns. The engine has no keys, constraints or indexes. |
| Azure Data Explorer (Kusto) | T-SQL INFORMATION_SCHEMA | Columns. No keys or indexes (engine); docstrings are not reachable from T-SQL: unknown. |
| RDS Data API | the Aurora engine's catalog (engine: 'postgres' | 'mysql') | As PostgreSQL / MySQL. |
Prorm instance that isIntrospectionUnsupportedError.import { Prorm } from 'ts-prorm-orm';
import { introspectSchema, generateModels, pullModels } from 'ts-prorm-orm/codegen';
const prorm = new Prorm({ dialect: 'postgres', url: process.env.DATABASE_URL });
await prorm.connect();
// 1. Read: a neutral SchemaModel.
const schema = await introspectSchema(prorm, { schema: 'public', tables: ['users', 'posts'] });
for (const table of schema.tables) {
console.log(
table.name,
table.columns.map((c) => `${c.name}: ${c.nativeType}`),
);
if (!table.known.checks) console.log(' (CHECK constraints unknown on this engine)');
}
// 2. Generate: files in memory. Pass the dialect so types are checked against it.
const { files, warnings } = generateModels(schema, {
style: 'define',
lang: 'ts',
dialect: prorm.getDialectInstance()!,
});
// Or both, plus writing (refuses to overwrite unless force: true):
const report = await pullModels(prorm, { out: 'src/models', style: 'class' });import fs from 'node:fs';
import path from 'node:path';
import { Prorm } from 'ts-prorm-orm';
import { introspectSchema, generateModels } from 'ts-prorm-orm/codegen';
const OUT = 'src/models';
const prorm = new Prorm({ dialect: 'postgres', url: process.env.DATABASE_URL, logging: false });
await prorm.connect();
try {
const schema = await introspectSchema(prorm, { schema: 'public' });
const { files, warnings } = generateModels(schema, {
style: 'define',
lang: 'ts',
dialect: prorm.getDialectInstance()!,
});
warnings.forEach((w) => console.warn(`! ${w}`));
const stale = files.filter((f) => {
const onDisk = path.join(OUT, f.path);
return !fs.existsSync(onDisk) || fs.readFileSync(onDisk, 'utf8') !== f.contents;
});
if (stale.length) {
console.error(`Models are out of date with the database: ${stale.map((f) => f.path).join(', ')}`);
console.error('Run: prorm models pull --out src/models --force, and review the diff.');
process.exitCode = 1;
}
} finally {
await prorm.close();
}$ DATABASE_URL=postgres://ci:ci@localhost/app npx tsx scripts/check-models.ts
Models are out of date with the database: Authors.ts
Run: prorm models pull --out src/models --force, and review the diff.schema against them withSchemaDiffer instead.descriptorToCode(descriptor) (aDataTypes call), resolveTypeCode(descriptor, { nativeType, dialect })parseDefault(raw, type, style), andtsString() — the one escaper every string from the catalog passes through on', \,*/ or ${ comes out as the same text and never as code.