Working with Legacy Databases¶
HyperDjango can integrate with existing PostgreSQL databases. The hyper inspectdb command reverse-engineers your database schema into HyperDjango Model classes, letting you access legacy data through the ORM without manual model writing.
Overview¶
The workflow for integrating a legacy database:
- Point HyperDjango at your existing database (via
DATABASE_URL). - Run
hyper inspectdbto generate Model classes from the schema. - Review and adjust the generated models.
- Use the models through the ORM as normal.
Connecting to an Existing Database¶
Set DATABASE_URL to your existing PostgreSQL connection string:
Or pass it directly to the command:
The inspectdb Command¶
hyper inspectdb connects to the database, reads the schema via PostgreSQL's information_schema and pg_catalog, and generates Python Model classes.
Basic Usage¶
# Inspect all tables in the public schema
hyper inspectdb
# Save output to a file
hyper inspectdb --output models.py
# Inspect specific tables only
hyper inspectdb --table users orders products
# Inspect a different schema
hyper inspectdb --schema inventory
# Include views and materialized views
hyper inspectdb --include-views
Example Output¶
For a database with users and orders tables:
"""
# Auto-generated by `hyper inspectdb`
# Schema: public
# Tables: 2
"""
from hyperdjango.models import Model, Field
class User(Model):
id: int = Field(primary_key=True)
email: str = Field(unique=True)
name: str = Field(nullable=True)
created_at: datetime = Field(nullable=True)
class Meta:
table_name = "users"
managed = False
class Order(Model):
id: int = Field(primary_key=True)
user_id: int = Field(foreign_key="User")
total: Decimal = Field()
status: str = Field(default="pending")
created_at: datetime = Field()
class Meta:
table_name = "orders"
managed = False
The managed = False setting tells HyperDjango not to create, modify, or delete the table -- it treats the schema as externally managed.
Type Mapping¶
inspectdb maps PostgreSQL types to Python types:
| PostgreSQL Type | Python Type |
|---|---|
int2, smallint |
int |
int4, integer, serial |
int |
int8, bigint, bigserial |
int |
float4, real |
float |
float8, double precision |
float |
numeric, decimal, money |
Decimal |
bool, boolean |
bool |
text, varchar, char, bpchar, name |
str |
uuid, json, jsonb, xml, inet, cidr, macaddr |
str |
timestamptz, timestamp |
datetime |
date |
date |
time, timetz |
time |
interval |
str |
bytea |
bytes |
Types not in the map default to str.
Handling Tables Without Primary Keys¶
PostgreSQL allows tables without explicit primary keys. When inspectdb encounters such a table, it generates a model without a primary_key=True field. You should add one manually:
# Generated (no PK detected)
class LegacyLog(Model):
message: str = Field()
created_at: datetime = Field()
class Meta:
table_name = "legacy_log"
managed = False
# Fixed: add a synthetic PK or use an existing unique column
class LegacyLog(Model):
ctid: str = Field(primary_key=True) # PostgreSQL physical row ID
message: str = Field()
created_at: datetime = Field()
class Meta:
table_name = "legacy_log"
managed = False
Alternatively, if there is a unique column that can serve as a PK, mark it:
class LegacyLog(Model):
log_id: int = Field(primary_key=True) # Was UNIQUE, promote to PK
message: str = Field()
created_at: datetime = Field()
class Meta:
table_name = "legacy_log"
managed = False
Foreign Keys and Constraints¶
inspectdb detects foreign key constraints and generates foreign_key references. Tables are topologically sorted so that referenced models appear before referencing models.
# inspectdb detects: orders.user_id -> users.id
class Order(Model):
id: int = Field(primary_key=True)
user_id: int = Field(foreign_key="User")
...
Circular Foreign Keys¶
If tables have circular FK dependencies (A references B, B references A), inspectdb falls back to alphabetical ordering. You may need to use string references:
class Department(Model):
id: int = Field(primary_key=True)
manager_id: int = Field(foreign_key="Employee", nullable=True)
class Employee(Model):
id: int = Field(primary_key=True)
department_id: int = Field(foreign_key="Department")
Unique Constraints and Nullable Columns¶
inspectdb reads constraint and column metadata:
UNIQUEconstraints becomeunique=Trueon the field.NOT NULLcolumns have nonullablemarker; nullable columns getnullable=True.DEFAULTvalues are included where detected.
Table and Column Naming¶
inspectdb converts table names to PascalCase class names and preserves column names as-is. If a column name is a Python reserved word (e.g., class, return, type), it is appended with _field:
# Table: user_sessions, column: class
class UserSession(Model):
id: int = Field(primary_key=True)
class_field: str = Field(column_name="class") # Renamed to avoid keyword conflict
Incremental Migration from Legacy Schema¶
To gradually take ownership of a legacy schema:
Step 1: Start Unmanaged¶
Generate models with managed = False. HyperDjango reads from and writes to the existing tables without altering the schema.
class Customer(Model):
id: int = Field(primary_key=True)
name: str = Field()
class Meta:
table_name = "customers"
managed = False # HyperDjango does not touch the schema
Step 2: Add New Columns via Migration¶
When you need new columns, switch to managed = True and use the migration system:
class Customer(Model):
id: int = Field(primary_key=True)
name: str = Field()
email: str = Field(nullable=True) # New column
class Meta:
table_name = "customers"
managed = True # Now HyperDjango manages this table
Step 3: Coexist with Other Applications¶
If the legacy database is shared with other applications, keep managed = False and apply schema changes through your existing migration tool (Flyway, Liquibase, raw SQL). HyperDjango models will pick up schema changes on restart.
Full Example: Connecting to an Existing Database¶
# app.py
from hyperdjango import HyperApp
from hyperdjango.database import Database
app = HyperApp("legacy_app")
@app.on_startup
async def setup():
db = Database("postgres://user:pass@db-host:5432/legacy_production")
await db.connect()
app.db = db
# models.py (generated by: hyper inspectdb --output models.py)
from hyperdjango.models import Model, Field
from datetime import datetime
from decimal import Decimal
class Product(Model):
id: int = Field(primary_key=True)
sku: str = Field(unique=True)
name: str = Field()
price: Decimal = Field()
created_at: datetime = Field()
class Meta:
table_name = "products"
managed = False
# routes.py
@app.route("GET", "/products")
async def list_products(request):
products = await Product.objects.order_by("-created_at").all()
return Response.json([p.to_dict() for p in products])
@app.route("GET", "/products/{sku}")
async def get_product(request, sku: str):
product = await Product.objects.get(sku=sku)
return Response.json(product.to_dict())
Inspecting Views and Materialized Views¶
Use --include-views to also generate models for database views:
Views are generated with managed = False (since they cannot be managed through model migrations). Materialized views can be refreshed via raw SQL:
See Also¶
- Database -- connection pool and query execution
- Models -- model definition and field types
- Migrations -- schema migration system
- CLI -- all
hypercommands includinginspectdb - PostgreSQL Extensions -- ArrayField, full-text search, and other PostgreSQL-specific features