PormG Field Types Reference
This comprehensive guide covers all field types available in PormG, inspired by Django ORM but optimized for Julia. Each field type maps to appropriate data types in PostgreSQL or SQLite and provides validation, constraints, and formatting capabilities.
Naming Conventions and Considerations
Field Naming Rules
- Recommended house style: lowercase snake_case —
username,email,created_at,first_name. PormG's own models and examples follow this, and it is the convention to prefer for new schemas. - Field-name case is preserved, not folded. Whatever case you declare is the field's identity and its database column. A field declared
driverIdregisters asdriverIdand maps to the column"driverId". This is what lets PormG faithfully target mixed-case / uppercase columns in existing (e.g. legacy Django) schemas. Field lookups are case-sensitive — query a field by the exact case you declared it. - Never use double underscores (
__) in field names or table names —__is the lookup separator (driverid__surname), so a field spelled with one would be unaddressable. - A field name may not start with an underscore.
_end = CharField()raisesModelDefinitionError. Name a column that is a Julia keyword — or that genuinely begins with an underscore — withdb_columninstead:
A single leading underscore used to be an escape hatch that PormG silently stripped (end_ = Models.CharField(db_column = "end") # field `end_` → column "end" id2 = Models.CharField(db_column = "_id") # field `id2` → column "_id"_enddeclared the columnend). It was retired in #317: it encoded the Julia identity and the SQL identity in one string, wheredb_columnstates them separately and composes withdb_table.idneeds nothing special — it is an ordinary Julia identifier.
Model Naming Rules
- Use snake_case with capitalized first letter:
Driver,Constructor,Pit_stop - Use singular nouns:
DrivernotDrivers,CircuitnotCircuits - Be descriptive and clear:
Driver_profile,Part_category,Race_result
Database Column Mapping
- By default, column names follow the field name verbatim, with case preserved: a field declared
firstNamebecomes the column"firstName"; declarefirst_nameto getfirst_name. - The house style is lowercase snake_case — prefer it for new schemas; reserve mixed-case declarations for faithfully mapping existing columns you don't control.
db_columnmaps a field to a differently-named column and is authoritative across DDL, queries, and migrations (#50) — e.g.chassis = CharField(db_column="chassis_code")keeps the fieldchassisbut targets the column"chassis_code". Supported on all field types exceptManyToManyField; see Schema Conventions.db_tableis the same idea one level up — a model option, not a field one, mapping a model to a differently-named (and, unlike a model name, arbitrarily-cased) table:Models.Model("driver_profile", db_table = "Driver_Profile_Legacy", …). Also authoritative across DDL, queries, and migrations (#59); see Pinning an explicit table name.
Examples of Good Naming
# ✅ Good field naming
Team_member = Models.Model(
id = Models.IDField(), # Plain identifier — no prefix needed
username = Models.CharField(max_length=30), # Lowercase
first_name = Models.CharField(max_length=50), # Snake_case
email_address = Models.EmailField(), # Descriptive
is_active = Models.BooleanField(), # Boolean prefix
created_at = Models.DateTimeField(), # Timestamp suffix
birth_date = Models.DateField() # Clear purpose
)
# ✅ Good model naming
Driver_profile = Models.Model(...) # snake_case with capital first letter
Part_category = Models.Model(...) # Clear relationship
Race_result = Models.Model(...) # Descriptive compound nameExamples to Avoid
# ❌ Bad naming practices
driver = Models.Model( # Should be capitalized
ID = Models.IDField(), # Should be id — house style is lowercase
firstName = Models.CharField(), # Should be first_name
Nationality__Code = Models.CharField(), # Never use __ (the lookup separator)
_end = Models.DateField(), # Retired escape hatch — raises ModelDefinitionError
end = Models.DateField() # Julia syntax error: `end` is a keyword
)
# ✅ the reserved-word column, said properly
driver = Models.Model("driver",
id = Models.IDField(),
end_ = Models.DateField(db_column = "end") # field `end_` → column "end"
)Primary Key Fields
IDField()
Purpose: Auto-incrementing 64-bit integer primary key.
Database Type:
- PostgreSQL:
BIGINTwithGENERATED AS IDENTITY - SQLite:
INTEGER PRIMARY KEY AUTOINCREMENT
Use Cases: Large-scale applications, future-proof primary keys, modern PostgreSQL features.
# Basic usage (most common)
Driver = Models.Model(
id = Models.IDField(),
surname = Models.CharField(max_length=50)
)
# With GENERATED ALWAYS (stricter identity)
Result = Models.Model(
id = Models.IDField(generated_always=true),
points = Models.DecimalField(max_digits=10, decimal_places=2)
)Key Parameters:
generated_always::Bool = false: Use GENERATED ALWAYS AS IDENTITY (stricter mode)primary_key::Bool = true: Always true for IDFieldauto_increment::Bool = true: Always true for IDField
Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
AutoField() — retired
AutoField was removed in favour of IDField. Calling Models.AutoField() raises FieldValidationError naming the replacement.
It was documented as a 32-bit INTEGER SERIAL key and never was one: the DDL renderer had no branch for it, so it emitted a TEXT column on both backends with no sequence, identity or AUTOINCREMENT behind it. A model keyed on it had a text primary key the database never populated, and makemigrations could never converge. An app that supplied its own key values would have worked; one that relied on the documented auto-increment never did.
Use IDField for every auto-incrementing integer key:
Part_category = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=100)
)IDField is BIGINT rather than INTEGER. An existing PostgreSQL column that really is integer is not re-typed — PormG compares the field's declared type, not the rendered width. It is not quite "nothing changes", though: a key column that is not a PostgreSQL IDENTITY column attracts an ADD GENERATED BY DEFAULT AS IDENTITY (a pre-existing :generated mismatch, not introduced by the retirement), and a column a real AutoField created is text. PostgreSQL refuses that as an identity column, so the migration errors — noisy but safe. SQLite does not refuse it: it rebuilds the table into INTEGER PRIMARY KEY AUTOINCREMENT, which aborts on a non-numeric key and silently renumbers a zero-padded one ('0042' becomes 42). Re-type such a column by hand. Run dry_run() before migrate(); the Upgrading guide has the details.
Repairing it would have bought a four-byte-narrower key on one backend — SQLite's INTEGER PRIMARY KEY is a 64-bit rowid alias, so the two were already physically identical there — while keeping a second integer key type that introspection cannot tell apart from IDField, which is its own class of never-converging migration. See the Upgrading guide.
UUID Fields
UUIDField()
Purpose: For storing Universally Unique Identifiers (UUIDs).
Database Type:
- PostgreSQL:
UUID(Native type) - SQLite:
TEXT
Use Cases: Distributed systems, secure primary keys, session tokens, unique object identifiers.
# UUID as a primary key
Api_session = Models.Model(
id = Models.UUIDField(primary_key=true, auto_add=true),
team_member_id = Models.ForeignKey("Team_member")
)
# UUID as a unique token
Access_token = Models.Model(
id = Models.IDField(),
team_member = Models.ForeignKey("Team_member"),
token = Models.UUIDField(unique=true, auto_add=true),
created_at = Models.DateTimeField(auto_now_add=true)
)Key Parameters:
auto_add::Bool = false: If true, automatically generates auuid4()on the application side when creating a new record without a provided value.primary_key::Bool = false: Can be used as a primary key.default::Union{String, Nothing} = nothing: A default UUID string.unique::Bool = false: Enforce uniqueness.
Text Fields
CharField(max_length)
Purpose: Variable-length strings with maximum length constraint.
Database Type: VARCHAR(max_length)
Use Cases: Names, titles, codes, short descriptions, enumerated values.
# Basic string fields
Team_member = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=30, unique=true),
email = Models.CharField(max_length=100, unique=true),
first_name = Models.CharField(max_length=50),
last_name = Models.CharField(max_length=50)
)
# Field with choices (enum-like behavior)
Store_order = Models.Model(
id = Models.IDField(),
status = Models.CharField(
max_length=20,
choices=(
("pending", "Pending"),
("processing", "Processing"),
("shipped", "Shipped"),
("delivered", "Delivered"),
("cancelled", "Cancelled")
),
default="pending"
)
)
# Field with a human-readable label (the column name follows the field name: "part_number")
Part = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=200),
part_number = Models.CharField(
max_length=50,
unique=true,
verbose_name="Part Number"
)
)Key Parameters:
max_length::Int = 250: Maximum characters (1 or greater; the backend sets the real ceiling)choices: Tuple of (value, display_name) pairsunique::Bool = false: Enforce uniquenessdb_index::Bool = false: Create database index
TextField()
Purpose: Unlimited length text content.
Database Type: TEXT
Use Cases: Articles, descriptions, comments, JSON data, large text content.
Race_report = Models.Model(
id = Models.IDField(),
title = Models.CharField(max_length=200),
content = Models.TextField(),
summary = Models.TextField(blank=true, null=true)
)
EmailField()
Purpose: Email addresses with built-in validation.
Database Type: VARCHAR with email validation
Use Cases: Team-member and driver emails, contact information, notification addresses.
Team_member = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=30),
email = Models.EmailField(unique=true),
backup_email = Models.EmailField(null=true, blank=true)
)
# For contact forms
Team_contact = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=100),
email = Models.EmailField(),
message = Models.TextField()
)URLField(max_length=200)
Purpose: For storing website addresses and URIs with character length validation.
Database Type: VARCHAR(max_length)
Use Cases: Profile links, social media URLs, external references.
Driver_profile = Models.Model(
id = Models.IDField(),
website = Models.URLField(max_length=500, null=true, blank=true),
instagram_profile = Models.URLField(unique=true)
)SlugField(max_length=50)
Purpose: Compressed strings typically used to build SEO-friendly URLs.
Database Type: VARCHAR(max_length)
Use Cases: Race-report slugs, part identifiers in URLs.
Best Practice: SlugField defaults to db_index=true as it is almost always used in filter() operations for routing.
Press_release = Models.Model(
id = Models.IDField(),
title = Models.CharField(max_length=200),
slug = Models.SlugField(unique=true)
)PasswordField()
Purpose: Django-compatible storage for password hashes.
Database Type: VARCHAR(128)
Use Cases: Persisting password hashes in tables that share a Django auth-style schema.
PasswordField is a VARCHAR(128) column sized to hold a Django-format password hash. It is a storage type only — PormG does not hash, verify, or otherwise transform the value. Hash the password in your application, store the finished string here, and read it back to verify. This keeps hashing policy in your app while the column stays wire-compatible with Django's authentication tables.
Expected storage format (Django PBKDF2-SHA256):
pbkdf2_sha256$720000$randomsalt$base64encodedHashNever assign a plain-text password to a PasswordField — the column stores whatever string it is given, verbatim. Hash the password in your application before saving.
# Team member account with password authentication
Team_member = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=150, unique=true),
email = Models.EmailField(unique=true),
password = Models.PasswordField()
)Key Parameters:
max_length::Int = 128: Column width for the stored hash (Django default)blank::Bool = false: Whether the field can be left blanknull::Bool = false: Whether NULL values are allowed
Hashing lives in your application
PormG ships no password hashing or verification. Generate the Django-format hash in your application (or a dedicated auth package) and assign the resulting string to the PasswordField; verify by re-hashing the candidate and comparing. Because the stored format matches Django's (pbkdf2_sha256$…), a table written this way stays readable by Django's own authentication code and vice versa.
Django Migration
If migrating from Django, password hashes are fully compatible. Users can continue logging in without any password reset required.
Numeric Fields
IntegerField()
Purpose: 32-bit signed integers for counts, quantities, and ratings.
Database Type: INTEGER
Range: -2,147,483,648 to 2,147,483,647
# Basic numeric data
Part = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=200),
stock_quantity = Models.IntegerField(default=0),
min_stock_level = Models.IntegerField(default=10)
)
# Rating systems
Fan_review = Models.Model(
id = Models.IDField(),
race = Models.ForeignKey("Race"),
rating = Models.IntegerField(), # 1-5 stars
helpful_votes = Models.IntegerField(default=0)
)
# Age and demographic data
Team_member = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=30),
age = Models.IntegerField(null=true, blank=true),
login_count = Models.IntegerField(default=0)
)BigIntegerField()
Purpose: 64-bit signed integers for large numbers and timestamps.
Database Type: BIGINT
Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
# Large counters and metrics
Season_analytics = Models.Model(
id = Models.IDField(),
page_views = Models.BigIntegerField(default=0),
unique_visitors = Models.BigIntegerField(default=0),
bytes_transferred = Models.BigIntegerField(default=0)
)
# Timestamp storage (Unix timestamp)
Timing_event = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=100),
timestamp_ms = Models.BigIntegerField(), # Milliseconds since epoch
driver_id = Models.BigIntegerField()
)FloatField()
Purpose: Double-precision floating-point numbers for measurements and calculations.
Database Type: DOUBLE PRECISION
# Telemetry measurements
Car_sensor = Models.Model(
id = Models.IDField(),
temperature = Models.FloatField(), # Celsius
humidity = Models.FloatField(), # Percentage
pressure = Models.FloatField() # hPa
)
# Geographic coordinates
Circuit = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=100),
latitude = Models.FloatField(),
longitude = Models.FloatField(),
elevation = Models.FloatField(null=true) # Meters above sea level
)
# Derived performance metrics (use DecimalField for currency)
Car_performance = Models.Model(
id = Models.IDField(),
pace_delta = Models.FloatField(), # Seconds vs pole
tyre_deg_rate = Models.FloatField(), # Seconds lost per lap
fuel_effect = Models.FloatField(null=true) # Seconds per 10 kg
)DecimalField(maxdigits, decimalplaces)
Purpose: Precise decimal numbers for financial and monetary data.
Database Type: NUMERIC(max_digits, decimal_places)
Use Cases: Currency, financial calculations, precise measurements.
# Financial data
Sponsor_invoice = Models.Model(
id = Models.IDField(),
subtotal = Models.DecimalField(max_digits=10, decimal_places=2),
tax_amount = Models.DecimalField(max_digits=8, decimal_places=2),
total_amount = Models.DecimalField(max_digits=10, decimal_places=2),
discount_rate = Models.DecimalField(max_digits=5, decimal_places=4) # 0.1234 = 12.34%
)
# Merchandise pricing
Merchandise = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=200),
unit_price = Models.DecimalField(max_digits=8, decimal_places=2),
wholesale_price = Models.DecimalField(max_digits=8, decimal_places=2),
weight = Models.DecimalField(max_digits=6, decimal_places=3) # Kilograms
)Key Parameters:
max_digits::Int: Total number of digitsdecimal_places::Int: Number of decimal places
Date and Time Fields
DateField()
Purpose: Calendar dates without time information.
Database Type: DATE
Format: YYYY-MM-DD
Current Contract:
- Accepts
Datedirectly. - Also accepts
DateTimeandZonedDateTime, coercing them to the calendar date before SQL generation. - Accepts
YYYY-MM-DDstrings. - This means
DateFieldis currently permissive; it does not yet reject datetime values automatically.
# Personal information
Team_member = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=30),
birth_date = Models.DateField(null=true),
registration_date = Models.DateField()
)
# Event scheduling
Grand_prix = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=200),
event_date = Models.DateField(),
registration_deadline = Models.DateField(null=true)
)
# Business records
Sponsor_contract = Models.Model(
id = Models.IDField(),
contract_number = Models.CharField(max_length=50),
issue_date = Models.DateField(),
due_date = Models.DateField(),
paid_date = Models.DateField(null=true)
)DateTimeField()
Purpose: Date and time with timezone support.
Database Type: TIMESTAMP WITH TIME ZONE
Use Cases: Timestamps, logs, audit trails, precise timing.
Current Contract:
defaultvalues are normalized toUnion{ZonedDateTime, DateTime, Nothing}.- Canonicalized to UTC (issue #79): every
DateTimeFieldvalue — written, bound, or used as a filter value — is canonicalized to one UTC ISO-8601 string,yyyy-mm-ddTHH:MM:SS.sss+00:00(millisecond precision,+00:00offset). This mirrors DjangoUSE_TZ/ Rails / SQLAlchemy and makes SQLite's lexicographic TEXT comparison agree with PostgreSQL'stimestamptzinstant comparison: equality and range filters return the same rows on both backends regardless of how the input instant is spelled (Zvs+00:00,.0/.000/no-subsecond, or a non-UTC offset such as-03:00/+05:30). - Passing
ZonedDateTimepreserves the instant (converted to UTC for storage) and is the recommended path for shared Django/PostgreSQL tables. - Passing a plain Julia
DateTimeis interpreted asUTC. - Internal
auto_nowandauto_now_addtimestamps are generated insettings.time_zoneand then canonicalized to UTC on serialization — the same instant, stored in the UTC spelling. - The same semantics are exercised on both PostgreSQL and SQLite integration backends, including
bulk_insertandbulk_updatepaths forDateTimeFieldvalues. - If your Django app uses
USE_TZ=Truewith a non-UTC active timezone, you should treat plainDateTimeas a deliberate UTC input and useZonedDateTimefor local civil times.
TIMESTAMPTZ vs TIMESTAMP
By default, DateTimeField uses TIMESTAMPTZ.
- TIMESTAMPTZ (Recommended): Stores values in UTC internally and converts them to your session's timezone upon retrieval. This ensures consistency across different geographical regions.
- TIMESTAMP: Stores the exact date and time provided without any timezone conversion. You can switch to this by passing
type="TIMESTAMP".
Naive vs Aware Inputs
- Aware input:
ZonedDateTime(2026, 3, 13, 9, 0, tz"America/Sao_Paulo")keeps the source timezone semantics explicit. - Naive input:
DateTime(2026, 3, 13, 9, 0)is currently serialized asUTC, not assettings.time_zone. - Interop rule: if the upstream system thinks in a local timezone, convert to
ZonedDateTimebeforecreate,update,bulk_insert, orbulk_update. - SQLite note: SQLite stores datetime values as text, but PormG reconstructs
ZonedDateTime/DateTimevalues on read so the high-level contract matches PostgreSQL.
# Audit and logging
Race_audit_log = Models.Model(
id = Models.IDField(),
team_member = Models.ForeignKey("Team_member"),
action = Models.CharField(max_length=100),
timestamp = Models.DateTimeField(),
ip_address = Models.CharField(max_length=45)
)
# Content management
Race_report = Models.Model(
id = Models.IDField(),
title = Models.CharField(max_length=200),
content = Models.TextField(),
created_at = Models.DateTimeField(),
updated_at = Models.DateTimeField(),
published_at = Models.DateTimeField(null=true)
)
# Team store
Store_order = Models.Model(
id = Models.IDField(),
created_at = Models.DateTimeField(),
shipped_at = Models.DateTimeField(null=true),
delivered_at = Models.DateTimeField(null=true)
)TimeField()
Purpose: Time of day without date information.
Database Type: TIME
Format: HH:MM:SS
# Facility hours
Team_store = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=100),
opening_time = Models.TimeField(),
closing_time = Models.TimeField()
)
# Scheduling
Garage_booking = Models.Model(
id = Models.IDField(),
date = Models.DateField(),
start_time = Models.TimeField(),
end_time = Models.TimeField(),
driver = Models.ForeignKey("Driver")
)
# Sports and timing
Race = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=100),
start_time = Models.TimeField(),
best_lap_time = Models.TimeField(null=true)
)DurationField()
Purpose: Time intervals and durations.
Database Type: INTERVAL
Use Cases: Elapsed time, durations, time spans.
# Task tracking
Pit_task = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=200),
estimated_duration = Models.DurationField(),
actual_duration = Models.DurationField(null=true)
)
# Media content
Onboard_video = Models.Model(
id = Models.IDField(),
title = Models.CharField(max_length=200),
duration = Models.DurationField(),
encoding_time = Models.DurationField(null=true)
)Boolean Fields
BooleanField()
Purpose: True/false values for flags and binary states.
Database Type: BOOLEAN
# Team member preferences and flags
Team_member = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=30),
is_active = Models.BooleanField(default=true),
is_staff = Models.BooleanField(default=false),
is_superuser = Models.BooleanField(default=false),
email_notifications = Models.BooleanField(default=true),
newsletter_subscription = Models.BooleanField(default=false)
)
# Content moderation
Race_report = Models.Model(
id = Models.IDField(),
title = Models.CharField(max_length=200),
content = Models.TextField(),
is_published = Models.BooleanField(default=false),
is_featured = Models.BooleanField(default=false),
allow_comments = Models.BooleanField(default=true)
)
# System settings
System_setting = Models.Model(
id = Models.IDField(),
maintenance_mode = Models.BooleanField(default=false),
registration_enabled = Models.BooleanField(default=true),
debug_mode = Models.BooleanField(default=false)
)Binary and File Fields
ImageField()
Purpose: Image file paths and metadata.
Database Type: VARCHAR (stores file path)
Use Cases: Race photos, galleries, driver avatars, car images.
# Driver profiles
Driver_profile = Models.Model(
id = Models.IDField(),
driver = Models.OneToOneField("Driver"),
avatar = Models.ImageField(null=true, blank=true),
cover_photo = Models.ImageField(null=true, blank=true)
)
# Merchandise catalog
Merchandise = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length=200),
main_image = Models.ImageField(),
thumbnail = Models.ImageField(null=true)
)
# Gallery system
Race_photo = Models.Model(
id = Models.IDField(),
title = Models.CharField(max_length=200),
image = Models.ImageField(),
caption = Models.TextField(blank=true),
upload_date = Models.DateTimeField()
)BinaryField()
Purpose: Raw binary data — images, compressed blobs, encrypted content.
Database Type:
- PostgreSQL:
BYTEA - SQLite:
BLOB
Use Cases: File storage, encrypted data, binary documents.
Handling: Raw bytes in, raw bytes out — write a Vector{UInt8} and read a Vector{UInt8} back. Arbitrary byte sequences round-trip intact, including 0x00 and payloads that are not valid UTF-8. An AbstractString is also accepted on write and stored as its UTF-8 code units; to store the decoded bytes of an encoded string, decode it yourself with hex2bytes(s) or base64decode(s).
Key Parameters:
max_length::Union{Int, Nothing} = nothing: maximum payload size in bytes, not characters. Enforced before the query is built and by aCHECKconstraint in the schema (octet_lengthon PostgreSQL,lengthon SQLite).nothingmeans unbounded.default::Union{Vector{UInt8}, Nothing} = nothing: rendered into the DDL as a byte literal ('\xdeadbeef'::bytea/X'deadbeef'). Must be aVector{UInt8}— aStringraisesFieldValidationErrorrather than guessing between its code units and a decoded encoding.
# Document storage
Technical_document = Models.Model("technical_document",
id = Models.IDField(),
name = Models.CharField(max_length=200),
file_data = Models.BinaryField(max_length=5_000_000), # ≤ 5 MB
mime_type = Models.CharField(max_length=100),
file_size = Models.IntegerField()
)
Technical_document.objects.create(
"name" => "2024 Monza aero package",
"file_data" => read("aero.pdf"), # Vector{UInt8}
"mime_type" => "application/pdf",
"file_size" => filesize("aero.pdf")
)
row = Technical_document.objects.filter("name" => "2024 Monza aero package").
values("file_data").list() |> first
write("roundtrip.pdf", row[:file_data]) # Vector{UInt8}, byte-identical
# Encryption and security
Encrypted_telemetry = Models.Model("encrypted_telemetry",
id = Models.IDField(),
team_member = Models.ForeignKey("Team_member"),
encrypted_content = Models.BinaryField(),
encryption_key_hash = Models.CharField(max_length=64)
)Earlier versions rendered BinaryField as TEXT on both backends. The next makemigrations after upgrading proposes a type change — ALTER … TYPE bytea USING convert_to(…, 'UTF8') on PostgreSQL, a table rebuild with CAST(… AS BLOB) on SQLite — which reinterprets the existing text as its UTF-8 bytes. If the column actually held encoded text (hex, Base64), substitute decode(col, 'hex') / decode(col, 'base64') in the generated plan before applying it. See UPGRADING.md.
Structured Data Fields
JSONField()
Purpose: Storing semi-structured data using JSON formatting.
Database Type:
- PostgreSQL:
JSONB(binary storage, fast querying, allows indexing) - SQLite:
TEXT(stores as a JSON string)
Use Cases: Configuration settings, variable data payloads, complex metadata.
Handling: In Julia, this field accepts and returns Dict or Vector types, automatically handling the serialization/deserialization.
Car_setup = Models.Model(
id = Models.IDField(),
settings = Models.JSONField(),
metadata = Models.JSONField(null=true, blank=true)
)
# Example usage:
Car_setup.objects.create("settings" => Dict("front_wing"=>5, "tyre_pressure"=>21.5))Relationship Fields
ForeignKey(to_model)
Purpose: Many-to-one relationships linking records to a single target record.
Database Type: BIGINT with foreign key constraint
Use Cases: Categories, users, parent-child relationships.
FK Value Contract:
- ForeignKey fields accept scalar primary-key values, including
0, when that key exists in the referenced table. - Use
nothingormissingto write SQLNULLon nullable FK columns.
# Press room
Race_report = Models.Model(
id = Models.IDField(),
title = Models.CharField(max_length=200),
author = Models.ForeignKey("Team_member", on_delete="CASCADE"),
category = Models.ForeignKey("Report_category", on_delete="PROTECT"),
content = Models.TextField()
)
# Team store
Store_order = Models.Model(
id = Models.IDField(),
customer = Models.ForeignKey("Fan", on_delete="PROTECT"),
shipping_address = Models.ForeignKey("Shipping_address", on_delete="SET_NULL", null=true),
total_amount = Models.DecimalField(max_digits=10, decimal_places=2)
)
Order_line = Models.Model(
id = Models.IDField(),
order = Models.ForeignKey("Store_order", on_delete="CASCADE"),
product = Models.ForeignKey("Merchandise", on_delete="PROTECT"),
quantity = Models.IntegerField(),
unit_price = Models.DecimalField(max_digits=8, decimal_places=2)
)
# Multiple ForeignKeys to the same model — related_name is optional, but recommended.
# Without it PormG derives one per field: `team_radio_sender` and `team_radio_recipient`.
Team_radio = Models.Model(
id = Models.IDField(),
sender = Models.ForeignKey("Team_member", on_delete="CASCADE", related_name="sent_messages"),
recipient = Models.ForeignKey("Team_member", on_delete="CASCADE", related_name="received_messages"),
content = Models.TextField(),
sent_at = Models.DateTimeField()
)On Delete Options:
CASCADE: Delete this record when target is deletedRESTRICT: Prevent deletion of target if this record existsSET_NULL: Set field to NULL (requiresnull=true)SET_DEFAULT: Set to the field's default value (requiresdefault=)PROTECT: Raise error to prevent deletionDO_NOTHING: No action (may cause integrity errors)
Omitting on_delete is also valid and is the default — PormG then emits no statement for the relation and the column renders ON DELETE NO ACTION, so the dependent row is not cascaded.
The two "requires" above are enforced, not advisory: registering a model with SET_NULL on a null=false field, or SET_DEFAULT with no default, raises ModelDefinitionError — and every such contradiction in the module is reported in that one error, naming each offending model, field and fix, so a schema carrying several of them is fixed in a single pass.
OneToOneField(to_model)
Purpose: Strict one-to-one relationships where each record corresponds to exactly one target record.
Database Type: BIGINT with unique foreign key constraint
Use Cases: Driver profiles, settings, model extensions.
# Driver profile extension
Driver_profile = Models.Model(
id = Models.IDField(),
driver = Models.OneToOneField("Driver", on_delete="CASCADE"),
bio = Models.TextField(blank=true),
birth_date = Models.DateField(null=true),
website = Models.CharField(max_length=200, blank=true),
location = Models.CharField(max_length=100, blank=true)
)
# Staff details
Staff_profile = Models.Model(
id = Models.IDField(),
team_member = Models.OneToOneField("Team_member", on_delete="CASCADE"),
staff_id = Models.CharField(max_length=20, unique=true),
department = Models.ForeignKey("Constructor"),
hire_date = Models.DateField(),
salary = Models.DecimalField(max_digits=10, decimal_places=2)
)
# Settings and preferences
Team_member_settings = Models.Model(
id = Models.IDField(),
team_member = Models.OneToOneField("Team_member", on_delete="CASCADE"),
theme = Models.CharField(max_length=20, default="light"),
language = Models.CharField(max_length=10, default="en"),
notifications_enabled = Models.BooleanField(default=true)
)A OneToOneField is a foreign key carrying a UNIQUE constraint, and every row-level rule is the same one (#418). It must be projected up front — reading an unprojected row.team_member raises LazyTraversalError, not a lazy load. values("team_member__username") traverses it like any other relation, row.team_member__username = "senna" is assignable, and save() writes that change to the Team_member table. Changing the key itself and a projected team_member__* column in the same save() is refused, because the projected update would filter on the key value already on the row — save the key change first.
ManyToManyField(to_model)
Purpose: Many-to-many relationships through a join table, without adding a column to either related model table.
When through is not supplied, PormG migrations synthesize a join table with two foreign keys and a composite unique index. The relation can be traversed in filters and projections with the same double-underscore syntax used by ForeignKey joins.
The field may also target the model that declares it (pass the model name as a string). Both join columns are then prefixed from_ / to_, since one table cannot carry the same column twice — see Self-Referential Relationships.
Driver_collection = Models.Model("driver_collections",
id = Models.IDField(),
label = Models.CharField(max_length=120),
drivers = Models.ManyToManyField(Driver, related_name="collections")
)
query = Driver_collection.objects
query.filter("drivers__nationality" => "Brazilian")
query.values("label", "drivers__surname")
rows = query.list()
driver_query = Driver.objects
driver_query.filter("collections__label" => "World champions")
driver_query.values("forename", "surname")
champions = driver_query.list()For write operations, bind the relation to a source primary key and use the relation manager:
collection_id = 1
senna_id = 102
prost_id = 117
manager = Driver_collection.drivers(collection_id)
manager.add(senna_id, prost_id) # returns nothing
manager.remove(prost_id) # returns nothing
manager.set([senna_id]) # returns (added=X, removed=Y)
driver_rows = manager.all().values("surname", "nationality").list()Use through=Existing_model when the relationship table has extra fields, such as the season when a driver was added to a collection. In that case PormG treats the through model as a normal model and does not auto-generate a join table — including its db_table, if it declares one, which then names the join table in every query and mutator, and its two foreign keys' db_column, which name the join columns (#377). The field-level db_table below applies to the auto-generated table only and is ignored when through is given.
Django-Style Strict Mutators: If the custom through model contains any extra fields beyond the relationship foreign keys, direct manager mutator operations (add, remove, clear, and set) will raise a QueryBuildError. Create or delete custom through model objects directly using the through model's objects manager instead.
Common Field Options
All field types support these common parameters:
Validation Options
null::Bool = false: Allow NULL values in databaseblank::Bool = false: Allow empty values in formsunique::Bool = false: Enforce uniqueness constraint on this single column. For uniqueness spanning two or more columns, use a model-levelUniqueConstraint— see Composite Uniqueness.default: Set default value for new records. PormG applies it only where the write supplies no value for the field; a field passed explicitly — including asnothing/missing— is honored as written. See Defaults and Auto Values for theDataFrameform of the same rule.
Database Options
db_index::Bool = false: Create a database index on this single column, for faster queries. To index two or more columns together, use a model-levelIndex— see Composite Indexes.db_column::Union{String, Nothing} = nothing: Maps this field to a differently-named physical column; authoritative across DDL, queries, and migrations (#50). Defaults to the field name (see Schema Conventions)db_constraint::Bool = true: Create database constraints (for relationships)
Example with All Common Options
Merchandise = Models.Model(
id = Models.IDField(),
# CharField with full options
name = Models.CharField(
max_length=200,
unique=true,
db_index=true
),
# Optional field with default
status = Models.CharField(
max_length=20,
choices=(("active", "Active"), ("inactive", "Inactive"))
),
# Nullable relationship
category = Models.ForeignKey(
"Merchandise_category",
on_delete="SET_NULL",
null=true,
blank=true
)
)Field Validation
PormG provides automatic validation for all field types:
Type Validation
# Integer fields validate numeric input
quantity = Models.IntegerField() # Only accepts integers
price = Models.DecimalField(max_digits=8, decimal_places=2) # Precise decimal
# String fields validate length
name = Models.CharField(max_length=50) # Max 50 characters
description = Models.TextField() # Unlimited length
# Date fields validate format
birth_date = Models.DateField() # Must be valid date
created_at = Models.DateTimeField() # Must be valid datetimeConstraint Validation
# Uniqueness validation
email = Models.EmailField(unique=true) # Must be unique across all records
# Choice validation
status = Models.CharField(
max_length=20,
choices=(("active", "Active"), ("inactive", "Inactive"))
) # Must be one of the choices
# Null validation
required_field = Models.CharField(max_length=100) # Cannot be NULL
optional_field = Models.CharField(max_length=100, null=true) # Can be NULLRelationship Validation
# Foreign key validation
author = Models.ForeignKey("Team_member", on_delete="CASCADE") # Must reference valid Team_member
# One-to-one validation
profile = Models.OneToOneField("Driver_profile") # Must be unique relationshipMigration Considerations
Safe Changes
These changes can be made without data loss:
- Adding new fields with
null=trueordefaultvalues - Increasing
max_lengthon CharField - Changing
blankfromfalsetotrue - Adding database indexes
- Changing
on_deletebehavior
Careful Changes
These changes require data validation:
- Decreasing
max_lengthon CharField - Changing
nullfromtruetofalse - Adding
unique=trueto existing fields - Changing field types (e.g., CharField to IntegerField)
Example Migration-Safe Model Evolution
# Version 1: Initial model
Team_member = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=30),
email = Models.CharField(max_length=100)
)
# Version 2: Safe additions
Team_member = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=30),
email = Models.CharField(max_length=150, unique=true), # Increased length, added unique
first_name = Models.CharField(max_length=50, blank=true), # New optional field
last_name = Models.CharField(max_length=50, blank=true), # New optional field
is_active = Models.BooleanField(default=true), # New field with default
created_at = Models.DateTimeField(null=true) # New nullable field
)Best Practices
Choosing the Right Field Type
- Use IDField for primary keys in new applications
- Use CharField for short text with known maximum length
- Use TextField for long content like articles or descriptions
- Use DecimalField for money and precise calculations
- Use IntegerField for counts and small numbers
- Use BigIntegerField for large numbers and timestamps
- Use DateTimeField for timestamps and audit trails
- Use ForeignKey for relationships between models
Performance Considerations
- Add indexes (
db_index=true) on frequently queried fields - Use appropriate field types to minimize storage
- Consider nullable fields for optional data
- Use choices for enumerated values instead of separate tables
- Avoid BinaryField for large files; use file paths instead
- Use UUIDField for distributed identity to avoid primary key collisions across systems
- Use JSONField for flexible metadata that does not require a rigid relational schema
This comprehensive guide covers all field types available in PormG. For specific implementation details and advanced usage, refer to the source code in src/Models.jl and the test examples in the test/ directory.