@qninhdt/typespec-sqlmodel
v0.7.0
Published
TypeSpec emitter that generates Python SQLModel classes from schemas decorated with @qninhdt/typespec-orm.
Maintainers
Readme
@qninhdt/typespec-sqlmodel
TypeSpec emitter that generates namespace-grouped SQLModel packages.
This emitter consumes @qninhdt/typespec-orm schemas and generates:
- SQLModel classes for
@table - Pydantic-style data models for default form models
- package scaffolding for standalone distribution
- Atlas-friendly package metadata via
atlas-provider-sqlalchemy
What This Emitter Is For
Use this emitter when you want TypeSpec to drive Python persistence models and form/data shapes with one shared schema contract.
It is designed for:
- namespace-derived Python package layouts
- explicit relation mapping
- strict persistence behavior that fails unsupported mappings by default
- easy migration setup through
target_metadata = SQLModel.metadata(consumed byatlas-provider-sqlalchemy)
Installation
sh
pnpm add -D \
@typespec/compiler \
@typespec/emitter-framework \
@alloy-js/core \
@alloy-js/typescript \
@qninhdt/typespec-orm \
@qninhdt/typespec-sqlmodel
Runtime Expectations
Generated Python output targets the SQLModel and SQLAlchemy ecosystem.
- standalone mode writes
pyproject.toml - every emitted namespace
__init__.pyexportstarget_metadata = SQLModel.metadata - many-to-many shorthand generates
__associations__.py - collection persistence uses PostgreSQL-oriented JSONB or ARRAY types depending on the configured strategy
The repo currently verifies generated output with Python 3.10+.
Configuration Reference
`yaml emit:
- "@qninhdt/typespec-sqlmodel"
options: "@qninhdt/typespec-sqlmodel": output-dir: "./outputs/sqlmodel" standalone: true library-name: "acme-models" collection-strategy: "jsonb" include: - "Demo.Platform" exclude: - "Demo.Platform.Audit" `
Supported options:
| Option | Type | Meaning |
| --------------------------- | ----------------------- | --------------------------------------------------------------------------------- |
| output-dir | string | target directory handled by the TypeSpec compiler |
| standalone | boolean | write pyproject.toml and package scaffolding |
| library-name | string | distribution name used in standalone mode |
| version | string | distribution version written to the standalone pyproject.toml (default 0.0.0) |
| description | string | optional description written to the standalone pyproject.toml |
| collection-strategy | "jsonb" \| "postgres" | persistence strategy for list-like fields |
| emit-atlas | boolean | when true, write atlas.hcl alongside the generated package (default: false) |
| include | string[] | namespace or declaration selectors to keep |
| exclude | string[] | namespace or declaration selectors to drop |
| auto-include-dependencies | boolean | when true, transitively pulls required dependencies into the selection |
Not supported:
module-name- emitter-specific folder aliases
Selector Behavior
SQLModel generation uses the shared ORM selector engine. Selectors are dotted names, not glob patterns.
Examples:
`yaml include:
- "Demo.GamePlatform" exclude:
- "Demo.GamePlatform.Audit" `
Behavior:
excludewins overinclude- redundant selectors warn
- excluding a required dependency fails emission before any Python package is written
Output Layout
Given:
typescript
namespace App.Identity;
Standalone output looks like:
text
outputs/sqlmodel/
pyproject.toml
app/
__init__.py
identity/
__init__.py
user.py
Rules:
- namespace segments become Python package directories
__init__.pyfiles are generated at every emitted package level- every package level (including multi-segment ones) exposes
target_metadata = SQLModel.metadata
That target_metadata export is the intended Atlas integration point. Atlas via atlas-provider-sqlalchemy auto-detects type changes and server defaults from the SQLAlchemy metadata, so no Alembic-style env.py configuration is required.
Generated Package Contract
Standalone output typically includes:
pyproject.toml- namespace package folders with generated
__init__.py - one module per emitted model
- a top-level
__associations__.pyfor shorthand many-to-many tables - a root package export for
target_metadata = SQLModel.metadata
Non-standalone mode emits only the code tree and skips package metadata files.
Schema Example
`typescript import "@qninhdt/typespec-orm";
using Qninhdt.Orm;
namespace Demo.Shared;
@tableMixin model Timestamped { @key id: uuid; @autoCreateTime createdAt: utcDateTime; @autoUpdateTime updatedAt?: utcDateTime; }
namespace Demo.Accounts;
@table model User { ...Demo.Shared.Timestamped; @unique @maxLength(320) @format("email") email: string;
@check("users_credits_non_negative", "credits >= 0") credits: int32 = 0;
@manyToMany("user_badges") badges?: Badge[]; }
@table model Badge { ...Demo.Shared.Timestamped; @unique code: string;
@manyToMany("user_badges") users?: User[]; } `
Generated Behavior
Tables
@table models become SQLModel classes with:
__tablename__- SQLAlchemy column types when needed
- index and unique metadata
- composite constraint support
- foreign-key handling with delete/update actions
Bare string properties must declare @maxLength(N) (or use the text scalar) — the emitter no longer falls back to a silent max_length=255 and reports string-without-max-length instead.
Data models
default form models become non-table Python models that preserve:
- validation metadata
- titles and descriptions
- placeholder metadata in JSON schema extras
Named checks
typescript
@check("users_credits_non_negative", "credits >= 0")
credits: int32 = 0;
becomes:
py
CheckConstraint("credits >= 0", name="users_credits_non_negative")
inside __table_args__.
Many-to-many shorthand
When both sides declare:
typescript
@manyToMany("user_badges")
the emitter generates:
- relationship fields using
secondary - a synthesized association table inside
__associations__.py - imports from that association module where needed
Alembic helper
Top-level package roots expose:
`py from sqlmodel import SQLModel
target_metadata = SQLModel.metadata `
This makes it straightforward to wire the generated package into Alembic.
Example:
py
from demo import target_metadata
Collection persistence
collection-strategy controls array storage:
"jsonb": JSON-backed list persistence"postgres": PostgreSQLARRAY(...)where supported
Unsupported persistence shapes fail with diagnostics.
Generated Relationship Model
What the emitter produces for common patterns:
- many-to-one and one-to-one relations become
Relationship(...)pairs with explicit ownership driven by@foreignKey - many-to-many shorthand becomes
secondary=...relationships backed by generated association tables - referenced-column foreign keys are preserved instead of assuming every relation points at
id
When you need payload columns on the join itself, define an explicit junction table model instead of relying on shorthand.
Supported Features
- namespace-first package layout
- standalone package generation with
pyproject.toml @tableMixin- referenced-column foreign keys
- named checks
- many-to-many shorthand
- collection persistence strategies
- default form models model generation
- shared filtering with
includeandexclude
Limitations
- many-to-many shorthand is for simple join tables without payload columns
- if a join table needs extra data, model it explicitly
- non-standalone mode emits code only and skips package metadata
- DB cascade actions are emitted on foreign keys; ORM
delete-orphanownership is not inferred unless a future explicit ownership decorator exists
Common Diagnostics And Gotchas
standalone-requires-library-nameStandalone mode needslibrary-nameto write a coherent Python distribution manifest.unsupported-typeThe TypeSpec field could not be mapped to a SQLModel or SQLAlchemy field and emission fails.missing-back-referenceA collection relation has no inverse owner. SQLAlchemy may require additional manual configuration if you keep the model shape as-is.foreign-key-target-not-table@foreignKeypoints at something that is not a@tablemodel.
Practical guidance:
- keep generated packages importable on their own before wiring them into application code
- use the exported root
target_metadatain Alembic instead of manually assembling model imports - prefer explicit data models for public-facing API shapes rather than exposing persistence models directly
Verification
The repo verifies generated Python output with:
sh
pnpm run compile-examples
python -m compileall outputs/file-vault/sqlmodel outputs/game-platform/sqlmodel
Related Docs
Made with heart by @qninhdt, with GPT-5.4 and Claude Opus 4.6.
