Fields
Fields define the columns in your database table. Each field maps a Python type to a SQL column, with full dialect support for MySQL and YugabyteDB.
All field types at a glance
| Field | Python type | MySQL | YugabyteDB |
|---|---|---|---|
IntField |
int |
INT |
INTEGER |
StrField(max_length=n) |
str |
VARCHAR(n) |
VARCHAR(n) |
TextField |
str |
TEXT |
TEXT |
BoolField |
bool |
TINYINT(1) |
BOOLEAN |
FloatField |
float |
FLOAT |
FLOAT |
DecimalField(p,s) |
Decimal |
DECIMAL(p,s) |
DECIMAL(p,s) |
DateField |
date |
DATE |
DATE |
DateTimeField |
datetime |
DATETIME |
TIMESTAMP |
JSONField |
dict/list |
JSON |
JSONB |
ForeignKeyField(to) |
int |
INT |
INTEGER |
Field options
Every field accepts these common options:
| Option | Type | Default | Description |
|---|---|---|---|
primary_key |
bool | False | Auto-increment PK |
nullable |
bool | True | Allow NULL |
default |
any | None | Default value |
unique |
bool | False | UNIQUE constraint |
index |
bool | False | Create index |
validators |
list | [] | Custom validators |
IntField
Maps to INT (MySQL) / INTEGER (YugabyteDB). Use for IDs, counts, ages, quantities.
from mydborm import BaseModel, IntField
class Product(BaseModel):
__tablename__ = "products"
id = IntField(primary_key=True) # AUTO_INCREMENT / SERIAL
quantity = IntField(nullable=False)
views = IntField(default=0)
rating = IntField(nullable=True)
SQL generated:
-- MySQL
id INT PRIMARY KEY AUTO_INCREMENT,
quantity INT NOT NULL,
views INT DEFAULT 0,
rating INT
-- YugabyteDB
id SERIAL PRIMARY KEY,
quantity INTEGER NOT NULL,
views INTEGER DEFAULT 0,
rating INTEGER
Validation examples:
from mydborm import IntField
f = IntField(nullable=False)
f.name = "quantity"
f.validate(42) # OK → 42
f.validate("10") # OK → coerced to 10
f.validate(None) # ERROR → ValueError: Field 'quantity' cannot be None
f.validate("abc") # ERROR → TypeError: wrong type
Real-world example:
class InventoryItem(BaseModel):
__tablename__ = "inventory"
id = IntField(primary_key=True)
product_id = IntField(nullable=False)
warehouse_id = IntField(nullable=False)
quantity = IntField(nullable=False, default=0)
reorder_at = IntField(nullable=True) # reorder when qty drops below this
item_id = InventoryItem.create(
product_id = 1,
warehouse_id = 5,
quantity = 100,
reorder_at = 10,
)
StrField
Maps to VARCHAR(n). Use for names, emails, codes — any text with a known max length.
from mydborm import StrField
class User(BaseModel):
__tablename__ = "users"
id = IntField(primary_key=True)
username = StrField(max_length=50, nullable=False, unique=True)
email = StrField(max_length=255, nullable=False, unique=True)
nickname = StrField(max_length=50, nullable=True, default="anonymous")
locale = StrField(max_length=5, nullable=True, default="en")
SQL generated:
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
nickname VARCHAR(50) DEFAULT 'anonymous',
locale VARCHAR(5) DEFAULT 'en'
Validation examples:
f = StrField(max_length=10, nullable=False)
f.name = "username"
f.validate("alice") # OK → "alice"
f.validate("a" * 11) # ERROR → exceeds max_length=10
f.validate(None) # ERROR → cannot be None
f.validate(123) # OK → coerced to "123"
Real-world example — product catalog:
class Product(BaseModel):
__tablename__ = "products"
id = IntField(primary_key=True)
sku = StrField(max_length=20, nullable=False, unique=True, index=True)
name = StrField(max_length=100, nullable=False)
brand = StrField(max_length=50, nullable=True)
color = StrField(max_length=30, nullable=True)
size = StrField(max_length=10, nullable=True)
category = StrField(max_length=50, nullable=True, index=True)
# Query by indexed fields is fast
laptops = Product.query().where("category", "Electronics").where("brand", "Apple").all()
TextField
Maps to TEXT. No size limit — use for long content like descriptions, blog posts, notes.
from mydborm import TextField
class Article(BaseModel):
__tablename__ = "articles"
id = IntField(primary_key=True)
title = StrField(max_length=200, nullable=False)
body = TextField(nullable=False) # unlimited
summary = StrField(max_length=500, nullable=True) # short excerpt
notes = TextField(nullable=True) # internal notes
aid = Article.create(
title = "Getting started with mydborm",
body = "This is a very long article... " * 1000,
summary = "A quick intro to mydborm",
)
Note
TextField has no max_length parameter. For searchable short strings use StrField. For long content use TextField.
BoolField
Maps to TINYINT(1) (MySQL) / BOOLEAN (YugabyteDB). Always use True/False — not 1/0.
from mydborm import BoolField
class User(BaseModel):
__tablename__ = "users"
id = IntField(primary_key=True)
active = BoolField(default=True)
verified = BoolField(default=False)
is_admin = BoolField(nullable=False, default=False)
newsletter = BoolField(nullable=True)
SQL generated:
-- MySQL
active TINYINT(1) DEFAULT 1,
verified TINYINT(1) DEFAULT 0,
is_admin TINYINT(1) NOT NULL DEFAULT 0
-- YugabyteDB
active BOOLEAN DEFAULT TRUE,
verified BOOLEAN DEFAULT FALSE,
is_admin BOOLEAN NOT NULL DEFAULT FALSE
Validation examples:
f = BoolField(nullable=False)
f.name = "active"
f.validate(True) # OK → True
f.validate(False) # OK → False
f.validate(None) # ERROR → cannot be None
f.validate("yes") # ERROR → TypeError: expects bool, got str
f.validate(1) # ERROR → TypeError: expects bool, got int
Dialect difference
MySQL stores booleans as TINYINT(1) and returns 1/0.
YugabyteDB returns native True/False.
mydborm normalises this — always use True/False in your code.
Real-world example — feature flags:
class FeatureFlag(BaseModel):
__tablename__ = "feature_flags"
id = IntField(primary_key=True)
name = StrField(max_length=50, nullable=False, unique=True)
enabled = BoolField(default=False)
beta_only = BoolField(default=True)
description = StrField(max_length=255, nullable=True)
# Toggle a feature
FeatureFlag.update({"enabled": True}, name="dark_mode")
# Get all enabled features
enabled = FeatureFlag.filter(enabled=True)
FloatField
Maps to FLOAT. Good for prices, scores, ratings. For money use DecimalField.
from mydborm import FloatField
class Product(BaseModel):
__tablename__ = "products"
id = IntField(primary_key=True)
price = FloatField(nullable=False)
weight = FloatField(nullable=True) # kg
rating = FloatField(nullable=True, default=0.0) # 0.0–5.0
Validation examples:
f = FloatField(nullable=False)
f.name = "price"
f.validate(9.99) # OK → 9.99
f.validate(10) # OK → 10.0 (coerced from int)
f.validate("bad") # ERROR → TypeError
f.validate(None) # ERROR → cannot be None
Real-world example — analytics:
class PageView(BaseModel):
__tablename__ = "page_views"
id = IntField(primary_key=True)
url = StrField(max_length=500, nullable=False)
load_time = FloatField(nullable=True) # seconds
scroll_pct = FloatField(nullable=True) # 0.0–100.0
bounce = BoolField(default=False)
# Average load time per URL
rows = db.fetchall(
"SELECT url, AVG(load_time) AS avg_ms FROM page_views GROUP BY url ORDER BY avg_ms DESC LIMIT 10"
)
DecimalField
Maps to DECIMAL(precision, scale). Use for money and anything where floating point errors matter.
from mydborm import DecimalField
class Order(BaseModel):
__tablename__ = "orders"
id = IntField(primary_key=True)
subtotal = DecimalField(precision=10, scale=2, nullable=False) # 99999999.99
tax = DecimalField(precision=10, scale=2, nullable=False)
discount = DecimalField(precision=10, scale=2, default=0.00)
total = DecimalField(precision=10, scale=2, nullable=False)
currency = StrField(max_length=3, default="USD")
Always use DecimalField for money
FloatField can have rounding errors: 0.1 + 0.2 = 0.30000000000000004.
DecimalField is exact: 0.1 + 0.2 = 0.3.
Real-world example:
from decimal import Decimal
oid = Order.create(
subtotal = Decimal("99.99"),
tax = Decimal("8.00"),
discount = Decimal("10.00"),
total = Decimal("97.99"),
currency = "USD",
)
order = Order.get(id=oid)
print(order["total"]) # Decimal('97.99') — exact!
DateField
Maps to DATE. Stores year, month, day — no time component.
from mydborm import DateField
class Employee(BaseModel):
__tablename__ = "employees"
id = IntField(primary_key=True)
name = StrField(max_length=100, nullable=False)
hired_on = DateField(nullable=False)
left_on = DateField(nullable=True)
birthday = DateField(nullable=True)
Real-world example:
from datetime import date
eid = Employee.create(
name = "Alice Smith",
hired_on = date(2024, 1, 15),
birthday = date(1990, 6, 20),
)
emp = Employee.get(id=eid)
print(emp["hired_on"]) # 2024-01-15
print(type(emp["hired_on"])) # <class 'datetime.date'>
# Query employees hired this year
import datetime
year_start = date(datetime.date.today().year, 1, 1)
new_hires = Employee.query().where("hired_on__gte", year_start).all()
print(f"New hires this year: {len(new_hires)}")
# Find employees with birthdays this month
this_month = date.today().month
# Use raw SQL for complex date functions
rows = db.fetchall(
"SELECT * FROM employees WHERE MONTH(birthday) = %s",
[this_month]
)
DateTimeField
Maps to DATETIME (MySQL) / TIMESTAMP (YugabyteDB). Full date + time.
from mydborm import DateTimeField
class AuditLog(BaseModel):
__tablename__ = "audit_logs"
id = IntField(primary_key=True)
user_id = IntField(nullable=False)
action = StrField(max_length=50, nullable=False)
table_name = StrField(max_length=50, nullable=False)
record_id = IntField(nullable=True)
created_at = DateTimeField(nullable=True)
Real-world example:
from datetime import datetime, timedelta
# Log an action
log_id = AuditLog.create(
user_id = 1,
action = "UPDATE",
table_name = "products",
record_id = 42,
created_at = datetime.now(),
)
# Find recent activity — last 24 hours
cutoff = datetime.now() - timedelta(hours=24)
recent = (AuditLog.query()
.where("created_at__gte", cutoff)
.order_by("created_at", desc=True)
.all())
print(f"Actions in last 24h: {len(recent)}")
# Serialise datetime to JSON
log = AuditLog.get(id=log_id)
j = log.to_json() # datetime auto-converted to ISO string
print(j)
# {"id": 1, "created_at": "2024-06-19T10:30:00", ...}
JSONField
Maps to JSON (MySQL) / JSONB (YugabyteDB). Store structured data — settings, metadata, tags.
from mydborm import JSONField
class UserProfile(BaseModel):
__tablename__ = "user_profiles"
id = IntField(primary_key=True)
user_id = IntField(nullable=False, unique=True)
settings = JSONField(nullable=False)
preferences = JSONField(nullable=True)
tags = JSONField(nullable=True)
metadata = JSONField(nullable=True)
Real-world examples:
# Store nested config
uid = UserProfile.create(
user_id = 1,
settings = {
"theme": "dark",
"language": "en",
"notifications": {
"email": True,
"push": False,
"frequency": "daily"
}
},
tags = ["premium", "beta-tester", "verified"],
metadata = {
"signup_source": "google",
"referral_code": "FRIEND50",
"last_login": "2024-06-19T10:00:00"
}
)
profile = UserProfile.get(id=uid)
# Access nested values
theme = profile["settings"]["theme"] # "dark"
email = profile["settings"]["notifications"]["email"] # True
first_tag = profile["tags"][0] # "premium"
# Update a nested value
settings = profile["settings"]
settings["theme"] = "light"
UserProfile.update({"settings": settings}, id=uid)
YugabyteDB JSONB advantage
YugabyteDB stores JSON as JSONB (binary) which is faster to query and
supports GIN indexes for searching inside JSON fields.
ForeignKeyField
Maps to INT — a reference to another table's primary key.
from mydborm import ForeignKeyField
class Author(BaseModel):
__tablename__ = "authors"
id = IntField(primary_key=True)
name = StrField(max_length=100, nullable=False)
class Book(BaseModel):
__tablename__ = "books"
id = IntField(primary_key=True)
title = StrField(max_length=200, nullable=False)
author_id = ForeignKeyField(to="Author", nullable=False)
category_id = ForeignKeyField(to="Category", nullable=True) # optional FK
price = FloatField(nullable=False)
SQL generated:
Real-world example — full e-commerce schema:
class Category(BaseModel):
__tablename__ = "categories"
id = IntField(primary_key=True)
name = StrField(max_length=50, nullable=False)
parent_id = ForeignKeyField(to="Category", nullable=True) # self-referential
class Supplier(BaseModel):
__tablename__ = "suppliers"
id = IntField(primary_key=True)
name = StrField(max_length=100, nullable=False)
country = StrField(max_length=50, nullable=True)
class Product(BaseModel):
__tablename__ = "products"
id = IntField(primary_key=True)
name = StrField(max_length=100, nullable=False)
sku = StrField(max_length=20, nullable=False, unique=True)
category_id = ForeignKeyField(to="Category", nullable=True)
supplier_id = ForeignKeyField(to="Supplier", nullable=True)
price = FloatField(nullable=False)
# Create all tables
Category.create_table()
Supplier.create_table()
Product.create_table()
# Seed
cat_id = Category.create(name="Electronics")
sup_id = Supplier.create(name="TechCorp", country="USA")
prod_id = Product.create(
name = "Laptop Pro",
sku = "LAPTOP-001",
category_id = cat_id,
supplier_id = sup_id,
price = 999.99,
)
# JOIN query
results = (Product.query()
.inner_join("categories", "products.category_id = categories.id")
.inner_join("suppliers", "products.supplier_id = suppliers.id")
.where("categories.name", "Electronics")
.all())
Custom validators
Attach validation rules to any field using the validators parameter.
Built-in validators
from mydborm import (
EmailValidator, # valid email format
UrlValidator, # valid http/https URL
RegexValidator, # custom regex pattern
RangeValidator, # numeric min/max
MinLengthValidator, # minimum string length
ChoiceValidator, # allowed values list
)
EmailValidator
from mydborm import StrField, EmailValidator
class Contact(BaseModel):
__tablename__ = "contacts"
id = IntField(primary_key=True)
email = StrField(max_length=255, nullable=False,
validators=[EmailValidator()])
# Valid
Contact.create(email="alice@example.com") # OK
Contact.create(email="user.name+tag@domain.co.uk") # OK
# Invalid
try:
Contact.create(email="not-an-email")
except ValueError as e:
print(e)
# Field 'email' must be a valid email address. Got: 'not-an-email'
try:
Contact.create(email="missing@domain")
except ValueError as e:
print(e)
# Field 'email' must be a valid email address. Got: 'missing@domain'
UrlValidator
from mydborm import StrField, UrlValidator
class Website(BaseModel):
__tablename__ = "websites"
id = IntField(primary_key=True)
url = StrField(max_length=500, nullable=False,
validators=[UrlValidator()])
Website.create(url="https://example.com") # OK
Website.create(url="http://example.com/path?q=1") # OK
try:
Website.create(url="example.com") # missing http://
except ValueError as e:
print(e) # Field 'url' must be a valid URL. Got: 'example.com'
try:
Website.create(url="ftp://example.com") # only http/https
except ValueError as e:
print(e) # Field 'url' must be a valid URL. Got: 'ftp://example.com'
RangeValidator
from mydborm import IntField, FloatField, RangeValidator
class Product(BaseModel):
__tablename__ = "products"
id = IntField(primary_key=True)
price = FloatField(nullable=False,
validators=[RangeValidator(min_val=0.01, max_val=99999.99)])
discount = IntField(nullable=True,
validators=[RangeValidator(min_val=0, max_val=100)])
rating = FloatField(nullable=True,
validators=[RangeValidator(min_val=1.0, max_val=5.0)])
Product.create(price=29.99, discount=10, rating=4.5) # OK
try:
Product.create(price=-5.00) # negative price
except ValueError as e:
print(e) # Field 'price' must be >= 0.01. Got: -5.0
try:
Product.create(price=10.0, discount=150) # discount > 100
except ValueError as e:
print(e) # Field 'discount' must be <= 100. Got: 150
RegexValidator
from mydborm import StrField, RegexValidator
class Product(BaseModel):
__tablename__ = "products"
id = IntField(primary_key=True)
sku = StrField(max_length=20, nullable=False,
validators=[RegexValidator(
r'^[A-Z]{2,4}-\d{4}$',
message="SKU format: 2-4 uppercase letters, dash, 4 digits (e.g. PROD-0001)"
)])
hex_color = StrField(max_length=7, nullable=True,
validators=[RegexValidator(r'^#[0-9A-Fa-f]{6}$')])
Product.create(sku="PROD-0001", hex_color="#FF5733") # OK
Product.create(sku="AB-1234", hex_color="#000000") # OK
try:
Product.create(sku="prod-0001") # lowercase not allowed
except ValueError as e:
print(e) # SKU format: 2-4 uppercase letters, dash, 4 digits (e.g. PROD-0001)
MinLengthValidator
from mydborm import StrField, MinLengthValidator
class User(BaseModel):
__tablename__ = "users"
id = IntField(primary_key=True)
username = StrField(max_length=50, nullable=False,
validators=[MinLengthValidator(3)])
password = StrField(max_length=255, nullable=False,
validators=[MinLengthValidator(8)])
User.create(username="alice", password="securepass123") # OK
try:
User.create(username="ab", password="securepass123")
except ValueError as e:
print(e) # Field 'username' must be at least 3 characters. Got: 2
ChoiceValidator
from mydborm import StrField, ChoiceValidator
STATUSES = ["pending", "processing", "shipped", "delivered", "cancelled"]
SIZES = ["XS", "S", "M", "L", "XL", "XXL"]
PRIORITIES = ["low", "medium", "high", "critical"]
class Order(BaseModel):
__tablename__ = "orders"
id = IntField(primary_key=True)
status = StrField(max_length=20, nullable=False,
validators=[ChoiceValidator(STATUSES)])
priority = StrField(max_length=10, nullable=False, default="medium",
validators=[ChoiceValidator(PRIORITIES)])
Order.create(status="pending", priority="high") # OK
try:
Order.create(status="unknown")
except ValueError as e:
print(e)
# Field 'status' must be one of ['pending', 'processing', 'shipped',
# 'delivered', 'cancelled']. Got: 'unknown'
Combining validators
from mydborm import StrField, MinLengthValidator, RegexValidator, ChoiceValidator
class UserAccount(BaseModel):
__tablename__ = "user_accounts"
id = IntField(primary_key=True)
username = StrField(max_length=30, nullable=False, validators=[
MinLengthValidator(3),
RegexValidator(r'^[a-zA-Z0-9_]+$',
message="Username may only contain letters, numbers and underscore"),
])
role = StrField(max_length=20, nullable=False, validators=[
ChoiceValidator(["admin", "editor", "viewer", "guest"]),
])
# All validators run in order — first failure stops
try:
UserAccount.create(username="ab", role="admin") # too short
except ValueError as e:
print(e) # Field 'username' must be at least 3 characters
try:
UserAccount.create(username="alice smith", role="admin") # space not allowed
except ValueError as e:
print(e) # Username may only contain letters, numbers and underscore
Cross-field validation with validators
class ShippingAddress(BaseModel):
__tablename__ = "shipping_addresses"
id = IntField(primary_key=True)
country = StrField(max_length=2, nullable=False)
postal_code = StrField(max_length=10, nullable=False)
state = StrField(max_length=50, nullable=True)
__validators__ = [
# US addresses require state
lambda data: (_ for _ in ()).throw(
ValueError("State is required for US addresses")
) if data.get("country") == "US" and not data.get("state") else None,
# US zip codes: 5 digits or 5+4
lambda data: (_ for _ in ()).throw(
ValueError("Invalid US zip code format")
) if (data.get("country") == "US" and
not __import__("re").match(r"^\d{5}(-\d{4})?$",
data.get("postal_code", ""))) else None,
]
# OK
ShippingAddress.create(country="US", postal_code="10001", state="NY")
ShippingAddress.create(country="GB", postal_code="SW1A 1AA")
# Fails — US needs state
try:
ShippingAddress.create(country="US", postal_code="10001")
except ValueError as e:
print(e) # State is required for US addresses