Skip to content

Repository files navigation

UVg

Crates.io License CI Matrix

Fast schema introspection for PostgreSQL, MySQL, SQLite, and MSSQL. Generates SQLAlchemy models, cross-dialect DDL, or migration diffs — with an interactive TUI for reviewing and applying changes. Drop-in replacement for sqlacodegen.

What UVg does

  • SQLAlchemy model generation — declarative ORM classes or Table() metadata, byte-for-byte compatible with sqlacodegen output.
  • Cross-dialect DDL — introspect one database and emit CREATE TABLE statements for a different engine, with automatic type translation (jsonbJSON, uuidUNIQUEIDENTIFIER, and so on across all 16 source×target permutations).
  • Schema diff — compare a source database against a target and emit ALTER TABLE statements to converge them.
  • Interactive TUI — enter two URLs, scroll through the generated diff, and apply it to the target with a confirmation prompt.
  • Fast — single static binary; ~10× faster than sqlacodegen on PostgreSQL and ~40× on MSSQL.

Installation

From crates.io

cargo install uvg

Prebuilt binaries

Download the archive for your platform from the latest release (Linux x86_64/aarch64, macOS aarch64, Windows x86_64), extract, and place uvg on your PATH.

From source

git clone https://github.com/johndauphine/uvg.git
cd uvg
cargo install --path .

Usage

UVg accepts SQLAlchemy-style URLs for both the source and (optional) target database:

postgresql://user:pass@host/db
mysql://user:pass@host/db
sqlite:///path/to/db.sqlite
mssql://user:pass@host/db

For production use, read Operations and Security before connecting to protected databases. It covers credential redaction, TLS settings, required privileges, logs/errors, generated artifacts, and safe apply workflows.

Generate SQLAlchemy models

# Modern declarative classes (default)
uvg postgresql://localhost/mydb -o models.py

# Core Table() metadata objects
uvg --generator tables postgresql://localhost/mydb -o models.py

# One file per table
uvg --split-tables --outfile models/ postgresql://localhost/mydb

# Filter specific tables
uvg --tables users,posts postgresql://localhost/mydb

Generate DDL in another dialect

Introspect a database and emit CREATE TABLE statements for a different engine. Types are translated automatically.

# PostgreSQL schema -> MySQL DDL
uvg postgresql://localhost/mydb --generator ddl --target-dialect mysql -o schema.sql

# MSSQL schema -> SQLite DDL, one file per table
uvg mssql://localhost/mydb --generator ddl --target-dialect sqlite --split-tables -o ddl/

Diff two schemas and generate a migration

Compare a source database against a live target and emit ALTER TABLE statements to converge them.

uvg postgresql://source/db mysql://target/db --generator ddl -o migration.sql

The target dialect is inferred from the target URL scheme. Same-dialect migrations converge cleanly — running the diff again after applying shows zero changes.

Versioned migrations

For Alembic-style workflows, UVg can write timestamped revision files and track the target database's current revision in a uvg_version table.

# Scaffold migrations/ (baseline + meta.yaml) and a profiles.yaml stub
# at ~/.config/uvg/profiles.yaml that `--profile` reads
uvg init

# Write migrations/<revision>_add-users-email.sql
uvg revision postgresql://source/db postgresql://target/db \
  --message "add users.email"

# Apply pending revisions to the target and update uvg_version
uvg upgrade postgresql://target/db

# Roll back one revision, or back to a named revision/base
uvg downgrade postgresql://target/db
uvg downgrade postgresql://target/db base

# Collapse multiple migration heads into one merge revision
uvg merge --message "merge active migration branches"

# Adopt an already-current target without running migration SQL
uvg stamp postgresql://target/db 20260519_141500 --yes

# Inspect the target's current revision
uvg current postgresql://target/db

# Show the local revision chain
uvg history
uvg history postgresql://target/db

Revision files use a simple SQL format:

-- uvg revision: 20260519_141500
-- parent: 20260518_120000
-- description: add users.email

-- UP
ALTER TABLE "users" ADD COLUMN "email" VARCHAR(255);

-- DOWN
ALTER TABLE "users" DROP COLUMN "email";

Optional hook sections run around the main change: -- PRE, -- UP, -- POST, then uvg_version is bumped. Downgrades run -- POST DOWN, -- DOWN, -- PRE DOWN before moving the recorded revision back. Generated irreversible down sections are marked with -- IRREVERSIBLE: and are refused by uvg downgrade until the user replaces them with real rollback SQL.

Branched histories are supported through explicit merge revisions. When uvg history shows multiple heads, run uvg merge --message <name> to write an empty multi-parent merge revision that restores a single head. Automatic downgrade through merge revisions is refused because uvg_version records one current revision; resolve that case manually and use uvg stamp.

Migration safety model

uvg upgrade and uvg downgrade parse-check migration SQL before applying it when the target dialect has a reliable parse-only mode. PostgreSQL uses a savepoint per statement inside one rolled-back transaction, so it catches syntax and catalog errors. SQL Server, MySQL, and SQLite do not expose a safe parse-only DDL mode for UVG's apply path, so UVG prints a one-line skip note and continues. Pass --no-parse-check before the subcommand to disable this phase explicitly.

Apply atomicity is per-dialect. On PostgreSQL, each apply batch and each migration section runs inside a single transaction: a failure rolls the batch back and leaves the target unchanged, and statements that cannot run inside a transaction block (CREATE INDEX CONCURRENTLY, VACUUM, ...) automatically fall back to statement-by-statement apply. On SQL Server, MySQL, and SQLite, DDL implicitly commits, so migration DDL is applied statement-by-statement and earlier statements in a failed migration may already be applied. Either way, on failure UVg stops at the first failed statement and prints the migration revision, section, statement number, dialect error, and failed SQL without printing the target URL — and says whether the section was rolled back. uvg_version is advanced only after all -- PRE, -- UP, and -- POST SQL succeeds; downgrades move or clear uvg_version only after all down SQL succeeds. See Operations and Security for the full guarantees.

Recovery is intentionally explicit: fix the target manually if needed, rerun an idempotent migration once the target is safe, or use uvg stamp <target-url> <revision> --yes only after verifying the schema already matches that revision. If a downgrade to base applied but clearing uvg_version failed, verify the target and clear uvg_version manually.

Statement retry is bounded and only applies to transient execution errors such as deadlocks, lock waits, and brief connection drops. Logical errors such as syntax errors, missing objects, and constraint failures are not retried. Generated rollback SQL marks unknown or destructive reversals with -- IRREVERSIBLE:; uvg downgrade refuses those sections until they are replaced with real rollback SQL.

Per-table migration layout (--out-dir)

The default DDL diff path writes one blob to stdout (or --outfile). For migrations you commit to git, --out-dir splits the output one file per table so git log -- migrations/users/ is the history of the users table.

uvg postgresql://source/db postgresql://target/db \
  --generator ddl \
  --out-dir migrations/ \
  --name initial

After a first run against a blank target:

migrations/
├── users/20260513T193000Z__initial.sql
├── posts/20260513T193000Z__initial.sql
└── _runs/20260513T193000Z__initial.json     # manifest of this run

Each .sql file starts with a provenance header so a reviewer can see what produced it without leaving the file:

-- Generated by uvg 1.5.0 on 2026-05-13T19:30:00Z (UTC)
-- Run:    20260513T193000Z__initial
-- Table:  public.users
-- Source: postgres  ->  Target: postgres

Re-run with no schema changes: nothing happens. uvg prints uvg: no schema changes to stderr, writes zero files, and exits 0. The directory is byte-identical — no new git diff.

Re-run after you add a column to users:

migrations/
├── users/20260513T193000Z__initial.sql
├── users/20260514T084500Z__add-email.sql    ← only new file
├── posts/20260513T193000Z__initial.sql
├── _runs/20260513T193000Z__initial.json
└── _runs/20260514T084500Z__add-email.json

Only the table that actually changed gets a new file. Unmodified tables stay untouched.

Notes:

  • Non-table-scoped DDL (enum CREATE TYPE, CREATE SCHEMA, etc.) lands in _schema/. Apply order is _schema/ first, then per-table files.
  • Subdirectory naming: <table> for default-schema tables, <schema>__<table> for non-default schemas (e.g. billing__orders/).
  • --name <SLUG> sets the suffix used in filenames; defaults to <source>_to_<target> (e.g. postgres_to_mysql).
  • --out-dir requires a target URL (it diffs source against target). For full DDL dumps, use --outfile or --split-tables.
  • If --outfile and --out-dir are both set, --outfile wins.

Interactive TUI

Launch the TUI to enter source and target URLs, review the generated diff, and apply it to the target with a confirmation prompt.

uvg -i                                              # prompts for URLs
uvg -i postgresql://source/db postgresql://target/db

When the diff spans multiple tables, the TUI shows a left tree pane (one entry per table, plus _schema for non-table-scoped DDL) and a right detail pane with the SQL for the selected node. Each node has a checkbox — toggle with Space, toggle all with A, then press a to apply only the checked nodes (_schema/ first, then tables in topological order).

Options

Flag Description
--generator <TYPE> declarative (default), tables, or ddl
--target-dialect <DIALECT> Target SQL dialect for DDL: postgres, mysql, sqlite, mssql
--split-tables Output one file per table (works with all generators)
--tables <LIST> Comma-delimited table names to include
--schemas <LIST> Schemas to introspect (default: public for PG, dbo for MSSQL, database name for MySQL)
--noviews Skip views
--options <LIST> noindexes, noconstraints, nocomments, nobidi, nofknames, noidsuffix, nosyntheticenums, nonativeenums, keep_dialect_types
--outfile <PATH> Output file or directory (default: stdout). Wins over --out-dir if both are set
--out-dir <DIR> Per-table migration layout for --generator ddl with a target URL. No-op runs write nothing — see above
--name <SLUG> Filename suffix used inside --out-dir (default: <source>_to_<target>)
--interactive, -i Launch interactive TUI for DDL diff and apply
--trust-cert Trust the server certificate (MSSQL only; see Operations and Security)

Output Examples

Declarative generator

from typing import Optional

import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass

class Users(Base):
    __tablename__ = 'users'

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(100))
    email: Mapped[str] = mapped_column(String(255), unique=True)
    bio: Mapped[Optional[str]] = mapped_column(Text)
    created_at: Mapped[Optional[datetime.datetime]] = mapped_column(
        DateTime(timezone=True), server_default=text('now()')
    )

Tables generator

from sqlalchemy import Column, Integer, MetaData, String, Table, Text

metadata = MetaData()

t_users = Table(
    'users', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(100), nullable=False),
    Column('email', Text),
    schema='public'
)

Supported Databases

PostgreSQL (tested: 16)

Scalars: bool, int2, int4, int8, float4, float8, numeric, text, varchar, char, bytea, date, time, timetz, timestamp, timestamptz, interval

Dialect types: uuid, json, jsonb, inet, cidr

Arrays: _int4, _text, and other array types via the ARRAY() wrapper

URL schemes: postgresql://, postgresql+psycopg2://

Microsoft SQL Server (tested: 2019, 2022)

Scalars: bit, tinyint, smallint, int, bigint, real, float, decimal, numeric, money, smallmoney

Strings: char, varchar, nchar, nvarchar, text, ntext (with collation)

Date/time: date, time, datetime, datetime2, smalldatetime, datetimeoffset

Binary: binary, varbinary, image

Dialect types: uniqueidentifier

URL schemes: mssql://, mssql+pytds://, mssql+pyodbc://, mssql+pymssql://

MySQL / MariaDB (tested: 8.0, 9.6)

Scalars: tinyint, smallint, mediumint, int, bigint (with unsigned variants), float, double, decimal

Strings: char, varchar, text, tinytext, mediumtext, longtext

Date/time: date, time, datetime, timestamp, year

Binary: binary, varbinary, blob, tinyblob, mediumblob, longblob

Special: json, enum, set, bit, boolean

Note: tinyint(1) is automatically mapped to Boolean.

URL schemes: mysql://, mysql+pymysql://, mysql+mysqldb://, mysql+aiomysql://, mysql+asyncmy://, mariadb://, mariadb+pymysql://

SQLite

SQLite uses type affinity. Recognized types: integer, smallint, bigint, real, float, double, numeric, decimal, text, varchar, char, blob, date, datetime, timestamp, time, boolean, json

Unknown types are mapped using SQLite affinity rules (contains "INT" -> Integer, contains "TEXT"/"CHAR" -> Text, etc.).

AUTOINCREMENT is detected from the CREATE TABLE SQL. CHECK constraints are parsed for synthetic enum generation.

URL schemes: sqlite:///relative/path, sqlite:////absolute/path, sqlite:///:memory:

Performance

Benchmarked against sqlacodegen 3.2.0 on the StackOverflow 2010 database (9 tables) using hyperfine:

Command Mean Min Max
sqlacodegen (PostgreSQL) 1.140s 1.099s 1.167s
uvg (PostgreSQL) 113.4ms 108.1ms 130.5ms
sqlacodegen (MSSQL) 1.187s 1.134s 1.231s
uvg (MSSQL) 29.9ms 28.1ms 32.1ms

10x faster on PostgreSQL, 40x faster on MSSQL.

Running tests

cargo test

Before opening a PR, run the same quality gates CI enforces:

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

For a local advisory view of Rust module size and test layout:

./scripts/module_size_report.py

This report is not a CI gate by default. It distinguishes approximate production lines from inline #[cfg(test)] lines and dedicated test files; see docs/module-size-report.md for the guideline.

CI also runs dependency, advisory, license, and source checks with cargo-deny. To run the same gate locally:

cargo install --locked cargo-deny
cargo deny --all-features --locked check advisories licenses bans sources

The live integration smoke tests are split into feature-focused test crates. PostgreSQL and MySQL introspection require a live database; SQLite runs in-memory:

# PostgreSQL introspection
DATABASE_URL=postgresql://user:pass@localhost/testdb cargo test --test introspect test_introspect_live_pg -- --ignored

# PostgreSQL versioned migration workflow on a disposable database
UVG_DISPOSABLE_PG_URL=postgresql://user:pass@localhost/testdb cargo test --test migrations test_versioned_migration_live_postgres_workflow_cli -- --ignored

# MySQL introspection
MYSQL_URL=mysql://user:pass@localhost/testdb cargo test --test introspect test_introspect_live_mysql -- --ignored

# SQLite (runs automatically, no server needed)
cargo test --test introspect test_introspect_sqlite_in_memory

Microsoft SQL Server end-to-end coverage is provided by the cross-dialect CRM matrix below.

Cross-dialect matrix

testdata/crm/ ships a 14-table CRM schema in three native dialects (MSSQL, PostgreSQL, MySQL) plus a runner that drives uvg through every (source, target) permutation. As of v1.5.0 all 9 pairs apply cleanly in ~7 seconds total. See testdata/crm/README.md for setup and expected counts.

cargo build --release
./testdata/crm/run_matrix.sh

Beta/RC real-schema validation

Before calling a release stable, maintainers run UVG against authorized real-world schemas and record the outcome in docs/beta-validation.md. The helper below creates a private validation bundle with generated models, DDL, optional diff/apply logs, and optional versioned migration logs. It records the Git commit, UVg version, and available database versions, syntax-checks generated Python, and imports it when SQLAlchemy is installed:

cargo build --release
scripts/beta_validate_schema.sh --help

For production-sized schema-evolution coverage, maintainers also run the StackOverflow2010 drift matrix in testdata/stackoverflow-drift/. The optional GitHub Actions workflow (run manually via workflow_dispatch) restores the SQL Server source database from STACKOVERFLOW2010_BAK_URL, applies committed drift packs, and requires SQL Server, PostgreSQL, MySQL, and SQLite targets to converge after every applicable pack.

Release process

Maintainers cut releases using docs/release.md. User-facing changes are tracked in CHANGELOG.md, and GitHub release archives include SHA-256 checksum files generated by the release workflow.

Acknowledgments

UVg is a reimplementation of sqlacodegen by Alex Grönholm, licensed under MIT. See NOTICE for full attribution.

License

Dual-licensed under either of:

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.

About

sqlacodegen rewritten in Rust - generates SQLAlchemy Python model code from MySql, Sqlite, PostgreSQL and Microsoft SQL Server databases

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages