Related tables¶
Getting started covers one spec on its own. Real
schemas come in sets, with keys between them, and the interesting question is
how you generate a consistent set: orders whose customer_id values are
customers that exist.
This tutorial builds three related specs and generates all of them in one
call. The complete version, with more columns and a spec loaded from YAML,
lives in examples/related_specs.py in the repository and runs in
CI, so it cannot go stale.
A parent¶
Nothing new here — a spec like any other. The unique=True on id matters
for what follows: it is what makes this a table other tables can point at.
import datetime as dt
import polars as pl
from polspec import ColSpec, ForeignKey, FrameSpec, Registry
class Customers(FrameSpec):
id = ColSpec(pl.Int64, bounds=(1, 10_000_000), unique=True)
name = ColSpec(pl.String, string_length=(3, 40))
country = ColSpec(pl.Enum(["UK", "US", "DE"]))
signed_up = ColSpec(pl.Date, bounds=(dt.date(2020, 1, 1), dt.date(2026, 1, 1)))
A child¶
__foreign_keys__ declares that customer_id only ever holds values that
exist in Customers.id.
class Orders(FrameSpec):
order_id = ColSpec(pl.Int64, bounds=(1, 100_000_000), unique=True)
customer_id = ColSpec(pl.Int64, bounds=(1, 10_000_000))
total = ColSpec(pl.Float64, bounds=(0.0, 6_000.0))
__foreign_keys__ = [
ForeignKey("customer_id", references=Customers, ref_columns="id"),
]
The two bounds have to agree
customer_id is declared (1, 10_000_000) — the same range as
Customers.id. That is not decoration. A key fills its column from the
parent, so the parent's domain has to fit inside the child's; declaring
bounds=(1, 50) here would be a contradiction, and polspec refuses it
when you write the class rather than when you run it.
A composite key¶
OrderLines points at Orders, and declares that no order has two lines
with the same number.
class OrderLines(FrameSpec):
order_id = ColSpec(pl.Int64, bounds=(1, 100_000_000))
line_no = ColSpec(pl.Int32, bounds=(1, 1_000_000))
quantity = ColSpec(pl.UInt16, bounds=(1, 500))
__unique_together__ = [["order_id", "line_no"]]
__foreign_keys__ = [
ForeignKey("order_id", references=Orders, ref_columns="order_id"),
]
Generating the set¶
A Registry holds the specs that belong together. resolve() binds every key
to its target and checks the set is coherent; order() is the parents-first
order the keys imply.
registry = Registry(Customers, Orders, OrderLines).resolve()
print(registry.order())
# ('Customers', 'Orders', 'OrderLines')
frames = registry.generate_all(1_000, seed=1)
generate_all walks that order and threads each parent frame into its
children, so every key is satisfied by construction:
orders = frames["Orders"]
customers = frames["Customers"]
assert set(orders["customer_id"]) <= set(customers["id"])
registry.validate_all(frames) # passes
Ask for different row counts per table by passing a mapping:
frames = registry.generate_all(
{"Customers": 1_000, "Orders": 5_000, "OrderLines": 20_000}, seed=1
)
Each spec's seed is derived from the registry seed and the spec's name, so adding a fourth table does not reshuffle the three you already had.
Drawing the result¶
to_mermaid() renders the set as an entity-relationship diagram — one entity
per spec, one line per key:
erDiagram
Customers {
Int64 id PK "bounds: [1, 10000000]"
String name "len: [3, 40]"
Enum country
Date signed_up "bounds: [2020-01-01, 2026-01-01]"
}
Orders {
Int64 order_id PK "bounds: [1, 100000000]"
Int64 customer_id FK "bounds: [1, 10000000]"
Float64 total "bounds: [0.0, 6000.0]"
}
OrderLines {
Int64 order_id UK "bounds: [1, 100000000]"
Int32 line_no UK "bounds: [1, 1000000]"
UInt16 quantity "bounds: [1, 500]"
}
Customers ||--o{ Orders : "fk_customer_id__Customers"
Orders ||--o{ OrderLines : "fk_order_id__Orders"
Where to go next¶
- Multiple specs — the rest of what
Registrydoes: discovery from a directory, one file for the whole set, shared categories. - Constraints — foreign keys in detail, including self-references and composite keys.
- Specs as files — moving a spec out of Python entirely, which the full worked example does for one of its tables.