API Reference
Overview
PormG provides a Django-inspired ORM for Julia with an async-first architecture. This page serves as a comprehensive reference for all exported functions, types, and the query builder API. For topic-specific guides, see the Reading and Writing sections.
Query Builder: Fluent API
PormG uses a Django-style, object-oriented query builder. All database operations start from Model.objects and are chained using methods that either modify the query or execute it.
# The general pattern (trailing-dot chain — a leading dot on the next line is a ParseError)
results = M.Driver.objects.
filter("nationality" => "Brazilian").
values("forename", "surname").
order_by("surname").
limit(10).
list()Chainable Methods
These methods modify the query builder and return the handler for further chaining:
| Method | Description | Example |
|---|---|---|
.filter(key => value, ...) | Add WHERE conditions (AND). Repeated calls accumulate, unlike .values()/.order_by() which replace. | .filter("nationality" => "British") |
.values("field1", "field2", ...) | Select specific columns. Use "*" for all main-table columns. | .values("*", "driverid__surname") |
.order_by("field", "-field") | Sort results. Prefix with - for descending. | .order_by("-points", "surname") |
.limit(n) | Limit the number of returned rows. | .limit(10) |
.offset(n) | Skip the first n rows. | .offset(20) |
.page(limit) / .page(limit, offset) | Limit, or limit and offset in one call. The one-argument form leaves any .offset() already set untouched. | .page(20, 40) / .page(20) |
.distinct() | Add DISTINCT to the SELECT. | .distinct() |
.db("key") | Route the query to a different connection pool. | .db("tenant_42") |
.on("path", key => value) | Add predicates to the ON clause of an existing join path. | .on("driverid", "nationality" => "British") |
.cjoin("field" => "Model", ...) | Add a custom join at query time. | .cjoin("driverid" => "Driver") |
.cjoin_on("Model"; alias, on, join_type) | Anchor-less join: on is the entire ON clause. | .cjoin_on("Driver"; alias = "d", on = [...]) |
.with("name" => subquery; join_field, join_type) | Define one CTE on the query; call again for a second. Its columns are then reached with CTE(name, path). | .with("fast" => sub) |
.select_for_update(; nowait, skip_locked, no_key) | SELECT … FOR UPDATE row lock (PostgreSQL; must run inside a transaction). | .select_for_update(nowait = true) |
.copy() | Deep-copy the handler to branch a chain without disturbing the original. | base.copy().filter("year" => 2020) |
See also: Custom Joins for .cjoin() / .cjoin_on() / .on(), and Subqueries and CTEs for .with() and the CTE reference object.
Queries that use .cjoin() must call .values(...) explicitly before execution. A bare SELECT * across joined tables causes DataFrames.jl to crash with ArgumentError: Duplicate variable names. PormG throws a clear error if you forget. Use .values("*", "joined_model__field") to quickly select all main-table columns plus specific fields from the joined table.
Terminal Methods
These methods finalize the query and execute it against the database:
| Method | Return Type | Description |
|---|---|---|
.list() | Vector{PormGRow} | Returns model-aware rows with dot-access and relationship accessors. |
.list(:dict) | Vector{Dict{Symbol, Any}} | Returns plain dictionaries for framework integrations that need real Dict values. |
.list(:json) | String | Returns results as a JSON string. |
query |> DataFrame | DataFrame | Pipe to DataFrame for tabular output. |
.count() | Int | Runs SELECT COUNT(*) and returns the count. |
.aggregate(alias => Agg(...), ...) | NamedTuple | Whole-queryset aggregation (no GROUP BY); returns one named tuple of scalars. See Aggregation. |
.exists() | Bool | Returns true if at least one row matches. |
.first() | PormGRow or nothing | Returns the first matching record or nothing. |
.last() | PormGRow or nothing | Returns the last matching record; inverts order_by, or falls back to primary-key descending when unset. |
.earliest(fields...) | PormGRow | Earliest row ordered by fields (ascending; "-field" flips); raises DoesNotExist when empty. |
.latest(fields...) | PormGRow | Latest row ordered by fields (descending; "-field" flips); raises DoesNotExist when empty. |
.get(filters...) | PormGRow | Returns exactly one row, or raises DoesNotExist / MultipleObjectsReturned. |
.create(key => value, ...) | PormGRow | Inserts a single record and returns it as a row (dot-access + .save()). |
.update(key => value, ...) | — | Updates all matching records. |
.get_or_create(lookup...; defaults) | (PormGRow, Bool) | Fetch-or-insert, never updates a match; returns (row, created). See Get or Create. |
.update_or_create(lookup...; defaults) | (PormGRow, Bool) | Row-level upsert: inserts on a fresh lookup or updates defaults on conflict; returns (row, created). See Update or Create. |
.delete() | — | Deletes all matching records. |
.inspect() | Dict | Full query metadata without executing — the inspect_query shape (see Query Inspection & Debugging below). |
PormGRow Instance Methods
Rows returned by .list(), .first(), .last(), .earliest(), .latest(), .get(), .create(), .get_or_create(), and .update_or_create() expose model-aware property access and instance-level persistence:
| Method | Return Type | Description |
|---|---|---|
row.field | value | Reads a selected field using normalized Julia-style field names. |
row[:field] | value | Reads a selected field by Symbol or String. |
row.pk | value | The row's primary-key value, via the model's declared pk column (any name, not just id). Throws if the model has no single-column pk. |
pk(row) / pk(row, default) | value | Function form (exported by using PormG); the 2-arg variant returns default instead of throwing (best-effort). |
row.relationship | ManyToManyManager | Accesses a many-to-many relationship manager when the model defines one. |
row.save() | PormGRow | Persists fields assigned on the row and clears its dirty state. |
row.save(show_query=:sql) | Vector | Returns planned UPDATE inspection payloads without executing or clearing dirty state. |
row.delete() | (Int, Dict) | Deletes this row through the shared deletion collector (cascades like query.delete()); returns (total, per-table counts). |
driver = M.Driver.objects.get("driverref" => "hamilton")
driver.nationality = "British"
driver.save()row.save() requires a model with exactly one primary key. Primary-key assignments are rejected, and a row fetched without the primary key selected cannot be saved.
Example:
# Full query chain with DataFrame output (trailing-dot chain)
df = M.Result.objects.
filter("driverid__nationality" => "Brazilian", "positionorder" => 1).
values("driverid__forename", "driverid__surname", "raceid__year").
order_by("-raceid__year") |> DataFrame
# Count and existence checks
n = M.Driver.objects.filter("nationality" => "British").count()
has_british = M.Driver.objects.filter("nationality" => "British").exists()Query Inspection & Debugging
show_query
An integrated switch available on all terminal methods to toggle between execution and inspection.
| Mode | Description |
|---|---|
:execute | Default. Executes the query and returns results. |
:sql | Returns the SQL string only. Minimal overhead for benchmarking. |
:dict | Returns full metadata dictionary (sql, parameters, dialect, model, operation, etc.). |
:inspection | Alias of :dict. Useful when you want the same rich metadata shape used by inspect_query(). |
:params | Returns the parameters array only. |
:none | Returns nothing. Zero-overhead mode for benchmarking the builder itself. |
query = M.Driver.objects.filter("nationality" => "British")
# Get just the SQL string
sql = query.list(show_query=:sql)
# Benchmark the builder without execution overhead
@time query.list(show_query=:none)
# Get full metadata
meta = query.list(show_query=:dict)inspect_query
Dedicated API for comprehensive query inspection without executing. Features a heuristic intent detector that guesses the operation type (select, insert, update) based on the object state.
query = M.Driver.objects.filter("nationality" => "Brazilian").order_by("surname")
inspection = query |> inspect_query()
println(inspection[:sql_text]) # The generated SQL
println(inspection[:parameters]) # Bound parameters
println(inspection[:operation]) # Automatically detects :select
println(inspection[:dialect]) # :postgresql or :sqliteLIMIT and OFFSET values are rendered as literal integers in the SQL string. They do not appear in inspection[:parameter_buckets] or inspection[:parameters]. This is by design.
Filter Operators
PormG uses __@ suffixes for lookup operators and field transforms:
Comparison Operators
| Operator | SQL Equivalent | Example |
|---|---|---|
field | = value | "nationality" => "British" |
field__@gt | > value | "points__@gt" => 10 |
field__@gte | >= value | "points__@gte" => 10 |
field__@lt | < value | "positionorder__@lt" => 3 |
field__@lte | <= value | "positionorder__@lte" => 10 |
field__@ne | <> value | "status__@ne" => "Retired" |
field__@in | IN (...) | "nationality__@in" => ["British", "French"] |
field__@nin | NOT IN (...) | "nationality__@nin" => ["British", "German"] |
field__@range | BETWEEN a AND b | "driverid__@range" => [1, 50] |
field__@isnull | IS NULL / IS NOT NULL | "dob__@isnull" => true |
field__@contains | LIKE '%val%' | "name__@contains" => "Monaco" |
field__@icontains | ILIKE '%val%' | "name__@icontains" => "monaco" |
Transform Functions
| Transform | Description | Example |
|---|---|---|
field__@year | Extract year from date | "dob__@year" => 1960 |
field__@month | Extract month from date | "dob__@month" => 3 |
field__@day | Extract day from date | "dob__@day" => 21 |
field__@quarter | Extract quarter (1-4) | "date__@quarter" => 1 |
field__@date | Extract date from datetime | "created_at__@date" => Date(2025, 1, 1) |
For the full list of operators and transforms, see Filters and Aggregates.
F-Expressions
F() enables database-side field references and arithmetic. Use it for field-to-field comparisons and computed expressions.
using PormG: F
using PormG.Functions: Sum, Count
# Field-to-field comparison
M.Result.objects.filter(F("grid") == F("positionorder"))
# Arithmetic in projections
M.Result.objects.values(
"driverid__surname",
"bonus" => F("points") * 0.1
)
# Aggregate ratios
M.Result.objects.values(
"driverid__surname",
"avg_pts" => Sum("points") / Count("resultid")
)
# Atomic update (no read-modify-write race)
M.Result.objects.filter("resultid" => 1).update("points" => F("points") + 10)
# Date arithmetic with explicit Julia durations (or the Interval helper)
using Dates
M.Race.objects.filter("raceid" => 1).update("date" => F("date") + (Month(1) + Day(15)))+/- accept Dates durations (Day, Month, Year, …) and Interval(...) for cross-database date math. See Field Expressions for the full reference.
Q Objects: Complex Boolean Logic
Q() and Qor() enable complex boolean predicates with AND/OR logic:
using PormG: Q, Qor
# AND logic (Q contains AND by default)
M.Driver.objects.filter(Q("nationality" => "Brazilian", "code" => "SEN"))
# OR logic
M.Driver.objects.filter(Qor("nationality" => "Brazilian", "nationality" => "French"))
# Nested AND/OR
M.Driver.objects.filter(
Q("nationality" => "Brazilian", Qor("forename" => "Ayrton", "forename" => "Nelson"))
)See Q Objects for the full reference.
Aggregate Functions
All aggregate functions can be used in .values() for grouping or combined with F-expressions:
| Function | SQL | Example |
|---|---|---|
Count("field") | COUNT(field) | "total" => Count("resultid") |
Sum("field") | SUM(field) | "total_pts" => Sum("points") |
Avg("field") | AVG(field) | "avg_pts" => Avg("points") |
Max("field") | MAX(field) | "best" => Max("points") |
Min("field") | MIN(field) | "worst" => Min("points") |
When aggregate values appear in values(), PormG automatically groups by the non-aggregated columns. Aggregate-based filters are promoted to HAVING.
# Wins per constructor with HAVING filter
df = M.Result.objects.values(
"constructorid__name",
"wins" => Count("resultid")
).filter(
"positionorder" => 1,
"wins__@gt" => 50
).order_by("-wins") |> DataFrameSQL Functions
PormG.Functions provides a comprehensive set of SQL functions:
String Functions
| Function | Description | Example |
|---|---|---|
Lower("field") | Convert to lowercase | "name_lower" => Lower("surname") |
Upper("field") | Convert to uppercase | "name_upper" => Upper("surname") |
Length("field") | String length | "name_len" => Length("surname") |
Concat(args...) | Concatenate values | "full" => Concat("forename", Value(" "), "surname") |
Trim("field") | Trim whitespace | "clean" => Trim("name") |
LTrim("field") | Left trim | "clean" => LTrim("name") |
RTrim("field") | Right trim | "clean" => RTrim("name") |
Replace("field", old, new) | Replace substring | "fixed" => Replace("name", "-", " ") |
Numeric Functions
| Function | Description |
|---|---|
Abs("field") | Absolute value |
Round("field", precision) | Round to precision |
Floor("field") | Floor |
Ceil("field") | Ceiling |
Sqrt("field") | Square root |
Exp("field") | Exponential |
Ln("field") | Natural logarithm |
Power("field", n) | Raise to power |
Mod("field", n) | Modulo |
Conditional & Utility Functions
| Function | Description | Example |
|---|---|---|
Value(x) | Literal value in SQL | Value("hello") |
Coalesce(args...) | First non-null value | Coalesce("nickname", "forename") |
NullIf("field", value) | Returns NULL if equal | NullIf("code", "") |
Greatest(args...) | Maximum of values | Greatest("points", Value(0)) |
Least(args...) | Minimum of values | Least("points", Value(100)) |
Cast("field", type) | Type casting | Cast("points", "INTEGER") |
Extract("field", "part") | Extract date/time part | Extract("dob", "year") |
ToChar("field", fmt) | Format to string | ToChar("dob", "YYYY-MM") |
Case Expressions
Binary (single condition): pass otherwise directly to When — no Case wrapper needed:
# CASE WHEN idade >= 60 THEN 'Sim' ELSE 'Não' END
"mais_60" => When("idade__@gte" => 60, then = "Sim", otherwise = "Não")Multi-branch: wrap a vector of When fragments in Case:
"category" => Case([
When("positionorder" => 1, then = "Winner"),
When("positionorder__@lte" => 3, then = "Podium"),
], default = "Other")Plain strings and numbers work as then, otherwise, and default values without Value().
See Functions and Dates for more details.
Custom Joins
.cjoin()
Defines custom join conditions at query time as a chainable method on the query handler. Useful for legacy databases, non-FK joins, and multi-tenant systems.
using PormG: Q, Qor
df = M.Result.objects.cjoin(
"driverid" => "Driver",
filters=[Q("nationality" => "Brazilian", Qor("forename" => "Ayrton", "forename" => "Nelson"))],
join_type="INNER"
).values("driverid__forename", "driverid__surname", "points") |> DataFrameParameters:
| Argument | Type | Description |
|---|---|---|
main_join | Pair{String,String} | "field" => "TargetModel" — the join path. |
filters | Vector | ON-clause predicates. Supports Pair, Q(), Qor(), F expressions. |
join_type | String | "LEFT" (default), "INNER", "RIGHT", or "FULL". |
field | PormGField | Optional custom field definition for non-FK joins. |
warn | Bool | Suppress auto-discovery warnings (default: true). |
See Custom Joins for the full documentation.
on()
Adds ON-clause predicates to existing join paths (including reverse joins) without redefining them:
query = M.Result.objects
query.on("driverid", "nationality" => "Brazilian", "code" => "SEN")
query.values("resultid", "driverid__surname", "points")Bulk Operations
bulk_insert
Inserts multiple records in a single operation. Returns the inserted rows.
df = DataFrame([
Dict("forename" => "Ayrton", "surname" => "Senna", "nationality" => "Brazilian"),
Dict("forename" => "Alain", "surname" => "Prost", "nationality" => "French"),
])
result = bulk_insert(M.Driver, df)Duplicates can be skipped or merged instead of erroring with the on_conflict= keyword (PostgreSQL and SQLite ≥ 3.24; see Conflict Handling):
bulk_insert(M.Status, df, on_conflict = :nothing) # ON CONFLICT DO NOTHING
bulk_insert(M.Status, df, on_conflict = (action = :nothing, target = ["statusid"])) # targeted skip
bulk_insert(M.Status, df, # upsert
on_conflict = (action = :update, target = ["statusid"], set = ["status"]))bulk_update
Updates multiple records in a single operation.
bulk_update(M.Result.objects, df_with_changes, columns=["points"], match_on=["resultid"])Key contracts:
columns=names the participating fields and is the single place a DataFrame column is mapped to a model field ("df_col" => "field");match_on=selects the per-row merge keys by bare model field name (a field in both is matched, never SET);filters=are constant predicates applied to every row.- A
match_onfield's source column is itscolumns=mapping when you declared one — the mapping is authoritative even if a same-named DataFrame column also exists (that case warns) — otherwise a DataFrame column with the field's own name. A value PormG auto-populates for an out-of-scope field (auto_now, or a staticdefaultwhencolumns=is omitted) is not a declared mapping: your same-named column outranks it, and with no column at all the call raises anUnknownFieldErrorrather than matching every row against one per-call constant. - A match key is matched, never written — it stays out of the
SETclause, so using anauto_nowfield as a key does not refresh that timestamp. - DataFrame columns are matched case-sensitively for both
columnsandmatch_on. A column that differs only in case from the model field (e.g.RaceIdvsraceid) raises an error naming the candidate; normalize headers withrename!(df, lowercase.(names(df)))or map explicitly with"DF_COL" => "field"incolumns=. - If
match_onis omitted, PormG infers the model primary key columns and uses those to identify rows. - A per-row match key passed in
filters=(a bare string or"df_col" => "field"pair) raises a migration error directing you tomatch_on=; there is no silent fallback. Likewise, the pre-#107 pair grammar inmatch_on=(["record_id" => "id"]) raises a migration error showing the rewrite (columns=[..., "record_id" => "id"], match_on=["id"]). Both migration errors are temporary deprecation aids and will be removed in a future release. bulk_update()rebuilds theWHEREclause frommatch_on=andfilters=and does not preserve filters that were already attached to the handler.- Constant lookup filters on base-table columns are supported, but relation traversals that would require JOINs are rejected.
- Foreign-key columns accept scalar primary-key values, including
0when that referenced row exists; usenothingormissingto write SQLNULLon nullable FK columns. - The same dry-run modes available elsewhere apply here:
:sql,:dict,:inspection,:params, and:none.
bulk_copy
⭐ PostgreSQL Only. Uses PostgreSQL's native COPY FROM STDIN protocol for ultra-fast bulk loading.
bulk_copy(M.Driver.objects, df)| Function | Best For | Speed | Protocol |
|---|---|---|---|
create() | Single rows | Standard | SQL INSERT |
bulk_insert() | Medium datasets (< 10k rows) | Fast | Multi-row INSERT |
bulk_copy() ⭐ | Massive datasets | Ultra-Fast | Postgres COPY |
bulk_update() | Modifying many rows | Fast | Multi-row UPDATE |
Transactions
run_in_transaction
Executes a block inside a database transaction with automatic commit/rollback:
PormG.run_in_transaction("db") do
M.Result.objects.create("raceid" => 1, "driverid" => 1, "points" => 25)
M.Driver.objects.filter("driverid" => 1).update("code" => "WIN")
# If any exception is raised, both operations are rolled back
endKey features:
- Async context propagation — spawned
@asynctasks inherit the transaction - Connection sharing — all queries in the block use the same connection
- Automatic rollback on error
See Transactions for the full reference including savepoints and multithreaded patterns.
atomic
Friendly, Django-flavored alias for run_in_transaction. A nested atomic on the same database automatically becomes a SAVEPOINT, so a failing inner block rolls back only to its savepoint while the outer transaction survives (works identically on PostgreSQL and SQLite):
atomic("db") do
M.Result.objects.create("raceid" => 1, "driverid" => 1, "points" => 25)
try
atomic("db") do # nested → SAVEPOINT
M.Result.objects.create("raceid" => 1, "driverid" => 1, "points" => 18)
error("validation failed") # rolls back to the savepoint only
end
catch
# outer transaction still usable here
end
endPass durable = true to require the block be the outermost transaction (raises if one is already active).
select_for_update
Query-builder method that adds a FOR UPDATE clause to lock the selected rows until the surrounding transaction commits — the guard for a safe read-modify-write. Keyword options nowait, skip_locked, and no_key map to FOR UPDATE NOWAIT / FOR UPDATE SKIP LOCKED / FOR NO KEY UPDATE (nowait and skip_locked are mutually exclusive). On PostgreSQL it must run inside a transaction; on SQLite it is a silent no-op (no row-level locking). See Row-Level Locking.
atomic("db") do
row = M.Constructor_standings.objects.
filter("constructorstandingsid" => 1).
select_for_update().
list() |> first
M.Constructor_standings.objects.
filter("constructorstandingsid" => row[:constructorstandingsid]).
update("points" => row[:points] + 25)
endAsync Execution
PormG is async-first internally: every terminal (list(), count(), create(), …) already yields to the Julia scheduler while the database round-trip is in flight. There is no separate async query API — wrap the ordinary call in a task:
t = Threads.@spawn M.Driver.objects.filter("nationality" => "Brazilian").list()
# ... other work overlaps the database round-trip ...
rows = fetch(t) # Base.fetch on the Task — same rows as calling list() directlyfetch_async / await_result (raw SQL)
The lower-level escape hatch accepts raw SQL only — not query-builder objects — and returns a FetchTask:
settings = PormG.Configuration.get_settings("db")
task = fetch_async(settings, "SELECT count(*) FROM driver")
# ... do other work ...
result = await_result(task) # returns rows and releases the pooled connectionBind user values with a plain array — write the backend-native placeholder ($1, $2 on PostgreSQL, ? on SQLite); PormG does no translation, and a NULL is missing:
# PostgreSQL
task = fetch_async(settings, "SELECT count(*) FROM driver WHERE nationality = \$1", ["Brazilian"])
n = await_result(task)
# SQLite: the same call with "... = ?" and the same ["Brazilian"] arrayfetch_async checks its connection out synchronously; only await_result returns it. Always await every task you start.
See the Async & Concurrency guide for fan-out patterns, @async vs Threads.@spawn, connection-pool sizing, and why you must not fan out queries inside a transaction.
Configuration API
Configuration.load(path; env=nothing)
Loads a database configuration folder. Use env to explicitly set the environment instead of relying on ENV["PORMG_ENV"].
PormG.Configuration.load("db"; env="prod")Configuration.load_many(paths; env=nothing)
Bootstraps multiple database folders in one call:
PormG.Configuration.load_many(["db", "db_analytics"]; env="prod")Configuration.is_loaded(path_or_key)
Returns true if PormG has registered settings for the given folder/key. Does not open connections.
Configuration.ping(path_or_key)
Tests actual database reachability. Returns Bool.
Configuration.status(path_or_key)
Returns a rich status payload:
s = PormG.Configuration.status("db")
# (key="db", loaded=true, reachable=true, adapter="PostgreSQL", app_env="prod")For the full configuration guide, see Configuration.
Advisory Locks
with_advisory_lock
Acquires a PostgreSQL advisory lock for distributed coordination:
PormG.with_advisory_lock("db", "migration_lock"; wait=true, timeout_ms=10000) do
# Critical section — only one process at a time
PormG.Migrations.migrate("db")
endStrategies:
:poll(default) — Client-side polling with interval:block— Server-side blocking viapg_advisory_lock
SQLite: no-op — the block executes without locking. It warns once per lock key; on_missing_lock=:ignore accepts that silently, on_missing_lock=:error raises BackendCapabilityError rather than running unprotected.
See Advisory Locks for the full reference.
Abstract Types
PormG's type hierarchy provides the foundation for the query builder and model system:
| Type | Description |
|---|---|
PormGAbstractType | Base abstract type for all PormG types. |
PormGSettings | The connection-settings/config type (Configuration.Settings is a subtype). Renamed from SQLConn. |
PormGBackend | Base for the database backend/dialect markers (subtypes: PormGPostgres, PormGSQLite), the dispatch key for SQL rendering and driver selection. |
SQLObject | Base for objects that can be stored in the database. |
SQLObjectHandler | Handles operations on SQL objects (the query builder). |
SQLTableAlias | Manages table aliases in SQL queries. |
SQLInstruction | Represents an instruction to build a SQL query. |
SQLType | Base for SQL-related expression types. |
SQLTypeField | Represents a field expression in queries. |
SQLTypeQ | Q-expression type (AND logic). |
SQLTypeQor | Qor-expression type (OR logic). |
SQLTypeF | F-expression type (field references). |
SQLTypeFunction | SQL function type. |
SQLTypeCTE | Supertype of CTE's reference object — a CTE column handle. |
PormGModel | Base for model types. |
PormGField | Base for field type definitions. |
PormGError | Root of the semantic error taxonomy (<: Exception). Every PormG misuse — querying, model definition, configuration, migrations, the pool — raises a subtype (see Error taxonomy); catch PormGError catches them all. |
Exported Symbols
This is the curated public surface. using PormG brings only the names below into scope — the SQL function constructors are not among them (see SQL function library).
Query Builder
object, get, Q, Qor, F, Exists, OuterRef, Subquery, CTE, Interval, show_query, inspect_query
Rows & exceptions
PormGRow, pk, DoesNotExist, MultipleObjectsReturned
Error taxonomy
Every PormG misuse raises a subtype of PormGError (<: Exception) so callers can catch a type instead of matching on a message string. Catch PormGError for any PormG failure, or a specific subtype for a specific reaction (#231, completed in #239):
PormGError, FieldAccessError, UnknownFieldError, LazyTraversalError, FilterError, QueryBuildError, UnsafeMutationError, InvalidValueError, WritesDisabledError, UnsupportedConnectionError, BackendCapabilityError, ProtectedError, DefinitionError, FieldValidationError, ModelDefinitionError, ConfigurationError, InvalidConfigurationError, MigrationError, InvalidMigrationError, PoolError, DatabaseError, IntegrityError, OperationalError, StatementError, TransactionError, error_message
The taxonomy has two halves. Most of it reports misuse of PormG, caught before anything is sent. DatabaseError reports what the database itself refused once a statement got there — a constraint violation, SQL the backend rejects, a connection dropped mid-query — so catch PormGError really does cover both, and an app never has to name SQLite.SQLiteException / LibPQ.Errors.* (which would mean depending on the driver package just to spell the type). The driver's own exception stays reachable on .cause (#268).
The subtypes are deliberately not <: ArgumentError. A catch ArgumentError block around a PormG call will not match — catch PormGError (or a specific subtype) instead.
Reading a caught error
Use error_message(e), not e.msg. Most subtypes carry a msg::String, but the ones built from structured fields — DoesNotExist, MultipleObjectsReturned, PoolTimeoutError, PoolConnectError, the three DatabaseError subtypes (IntegrityError, OperationalError, StatementError), and DestructiveMigrationError (which renders its statements too) — do not render from msg alone, so reaching for that field breaks on exactly the errors you are least likely to have tested against. See Error Handling for the per-type field list.
try
M.Result.objects.filter("driverid__surname" => "Senna").update("points" => 25)
catch e
e isa PormGError || rethrow()
@error "PormG rejected the write" msg=error_message(e) type=typeof(e)
enderror_message is defined through showerror, which every subtype implements, so it stays correct for subtypes added later. It never returns less than e.msg: for most subtypes it is exactly that field, and for the few with their own showerror it returns the richer rendering.
Querying
| Type | Raised when |
|---|---|
FieldAccessError (abstract) | Umbrella for field/accessor lookup failures — catch it to get both cases below. |
UnknownFieldError | A field, alias, column, or __ lookup path does not exist on the model or projected row. |
LazyTraversalError | An unprojected ForeignKey or OneToOneField was read off a fetched row — project it in values(...) first. |
FilterError | Invalid filter argument/shape, or an operator misused on a JSON/subquery column. |
QueryBuildError | Structural/API misuse while building a query (joins, CTEs, projection, ordering, window/bulk config). The long-tail default — it is the bucket for query-shape misuse that isn't one of the sharper categories, so catch QueryBuildError says little beyond "PormG rejected the query shape". Catch a sharper subtype when you need to branch on the cause. |
UnsafeMutationError | An update()/delete() was requested without a filter (or another unsafe shape). |
ProtectedError | A delete() was refused because rows reference the target through a ForeignKey with on_delete = PROTECT/RESTRICT — the data forbids it; delete or reassign the referencing rows first. |
BackendCapabilityError | The active backend cannot do this: PG-only lookups on SQLite (JSONB, iunaccent_*), explicit window frame= on SQLite, bulk_copy on SQLite, with_advisory_lock(...; on_missing_lock = :error) on SQLite, or a too-old SQLite library. Change the query or the backend. |
InvalidValueError | A value failed coercion/type validation on insert/update, an identifier failed the safety check, or an interval/duration could not be parsed. |
WritesDisabledError | The connection is not permitted to insert/update/delete — change_data: false in connection.yml, which is why it lives under ConfigurationError. (Renamed from PermissionError in the pre-publish naming pass.) |
UnsupportedConnectionError | A connection object that is neither PostgreSQL nor SQLite reached an execution path — an internal PormG dispatch bug; please report it. (Capability limits are BackendCapabilityError; an unbound model is InvalidConfigurationError.) |
DoesNotExist / MultipleObjectsReturned | get() found zero / more than one row. |
Defining models
| Type | Raised when |
|---|---|
DefinitionError (abstract) | Umbrella for definition-time failures — catch it to get both cases below. One include("models.jl") can raise either, so a handler naming only one silently misses the other. |
FieldValidationError | A field constructor got an invalid argument — a kwarg of the wrong type, an out-of-range max_length, a default that violates the field's own contract, or a field type that cannot be a primary key. |
ModelDefinitionError | A model/schema definition is invalid — more than one primary key, a duplicate related_name, an illegal field name, a UniqueConstraint or Index naming an unknown field, or an unresolvable ForeignKey target. |
FieldValidationError fires while defining a model; InvalidValueError fires while coercing a value on the insert/update path. That is the distinction between the two.
Configuration and migrations
| Type | Raised when |
|---|---|
ConfigurationError (abstract) | Umbrella for configuration failures — covers InvalidConfigurationError, MissingConfigurationError, and WritesDisabledError (listed in the Querying table above, where users meet it). |
InvalidConfigurationError | Configuration is present but unusable — unsupported adapter, unknown connection key, malformed extensions, a model not bound to a connection (or bound to an entry whose pool was never built), a missing driver package, or an attempt to overwrite a static connection. |
MissingConfigurationError | No configuration folder / connection.yml, or the selected environment has no matching block. Not on the using PormG surface — name it PormG.Configuration.MissingConfigurationError. |
MigrationError (abstract) | Umbrella for migration-engine failures — catch it to get both cases below. |
InvalidMigrationError | A duplicate index name in a plan, an invalid answer to an interactive makemigrations prompt, or an unimplemented migrate_to(version) path. |
DestructiveMigrationError | A destructive plan was applied non-interactively without destructive=true; carries the refused statements. Not on the using PormG surface — name it PormG.Migrations.DestructiveMigrationError. |
PoolError (abstract) | Umbrella for connection-pool failures — catch it to get both cases below. |
PoolTimeoutError / PoolConnectError | The pool is saturated / a physical connection could not be opened. Both carry structured fields (adapter, pool_size, attempts, …) rather than a msg — read them with error_message. |
Database errors
Everything above reports misuse of PormG, raised before a statement leaves the process. These report what the database refused once it got there. Each carries adapter ("PostgreSQL" / "SQLite") and cause — the driver's own exception, kept so SQLSTATE-level detail stays reachable — instead of a msg, so read them with error_message.
| Type | Raised when |
|---|---|
DatabaseError (abstract) | Umbrella for every failure raised by the database itself. catch DatabaseError covers all three below without naming a driver package. |
IntegrityError | A constraint said no — UNIQUE, FOREIGN KEY, NOT NULL, CHECK, or an exclusion constraint. The one database failure applications routinely handle rather than propagate. |
OperationalError | Transient, and retrying may succeed — the connection dropped mid-query, a deadlock, a serialization failure, or a lock that could not be acquired (including a with_advisory_lock timeout). |
StatementError | The statement could not be executed — invalid SQL, unknown table/column, a rejected type, or insufficient privileges. Also the landing type for anything the backend could not classify, so the umbrella has no holes. |
TransactionError | Not a database error: the transaction API was used in a way that cannot work — atomic(durable=true) nested inside an open transaction, or touching a model bound to one connection while a transaction is open on another. Nothing was sent. |
try
M.Driver.objects.create("driverref" => "senna", "code" => "SEN")
catch e
e isa IntegrityError && return conflict(error_message(e)) # a constraint refused it
e isa OperationalError && return retry_later() # transient — try again
rethrow()
endLibPQ parameterizes its exception type on the SQLSTATE, so on PostgreSQL the kind is read straight off the error code. SQLite.SQLiteException carries only a message, so on SQLite the kind comes from SQLite's own literal constraint strings ("UNIQUE constraint failed", …) — stable, but not a code. Treat IntegrityError as reliable on both; .cause is there when you need more than PormG's three kinds.
Connect-time failure is not a DatabaseError — it never reached a statement, and arrives as PoolConnectError under PoolError. A failed migration ALTER TABLE is one: migrate() lets the StatementError through rather than re-wrapping it as MigrationError, which would bury the constraint detail — so catch DatabaseError alongside MigrationError around migrate().
The abstract umbrellas exist so the buckets have no holes: catch ConfigurationError also catches a missing connection.yml, and catch MigrationError also catches a refused destructive migration.
try
M.Result.objects.update("points" => 25) # no filter → refused, protects every row
catch e
e isa PormG.UnsafeMutationError && @warn "add a filter before update()"
e isa PormG.WritesDisabledError && @warn "this connection is read-only"
rethrow(e)
endReacting to a whole category — the umbrellas make one catch enough:
try
PormG.Configuration.load("db_2")
PormG.Migrations.migrate("db_2")
catch e
e isa PormG.ConfigurationError && @error "check connection.yml" exception=e
e isa PormG.MigrationError && @error "migration refused" exception=e
rethrow(e)
endBulk Operations
bulk_insert, bulk_update, bulk_copy, allocate_primary_keys, resync_sequences
Async API
fetch_async, await_result, FetchTask — see Async & Concurrency
Transactions
run_in_transaction, atomic, with_savepoint, with_tx_context, in_transaction_context — plus PormG.ConnectionPool.with_transaction for hand-rolled lifecycles
Locking
with_advisory_lock
Connection pool
pool_stats, PoolTimeoutError, PoolConnectError — manual checkout via PormG.ConnectionPool.acquire_connection / release_connection (pair them in a finally; on SQLite a write needs mode = :write)
Utilities & lifecycle
upgrade_guide, register_ignore_tables!, @import_models, @models_module, @pormg_debug
The one-off lifecycle helpers PormG.setup() (interactive project wizard) and PormG.install_ai_skills() are deliberately not exported — their generic names would otherwise land in every using PormG namespace. Call them qualified, exactly as shown throughout these docs.
The low-level fetch(settings, sql; params=[...]) escape hatch (values bound with backend-native placeholders — see Async & Concurrency) is a method of Base.fetch, so it is always in scope (no import, no qualification) and does not shadow Base. Prefer the fluent terminals (list(), DataFrame, count()) for application code.
SQL function library: PormG.Functions
The aggregate, conditional, window, string and math constructors live only in the PormG.Functions submodule — they are not exported into Main, and there is no PormG.Sum alias either. Their names (Sum, Count, Max, Round, Replace, Length, …) are generic enough to collide with Base and user code, so the library has exactly one home. Reach it either way:
using PormG, PormG.Functions # bring the whole library into scope
using PormG.Functions: Sum, Count # …or just the ones you use
M.Result.objects.values( # …or qualify without importing
"n" => PormG.Functions.Count("resultid"))bulk_*, Q, Qor, F, Exists, OuterRef, Subquery, CTE, Interval stay at the top level — they are query primitives, not part of the function library.
The library in full — the same index ?PormG.Functions prints in the REPL:
Aggregate — Sum, Avg, Count, Max, Min — see Filters and Aggregates
Conditional — Case, When — see Functions and Dates
Window — WindowOver, WindowSpec, Rank, DenseRank, RowNumber, Lag, Lead, FirstValue, LastValue, NthValue — see Window Functions
String — Concat, Lower, Upper, Length, Replace, Trim, LTrim, RTrim
Math — Abs, Round, Floor, Ceil, Sqrt, Exp, Ln, Power, Mod
Type / value — Cast, Extract, ToChar, Value, Coalesce, Greatest, Least, NullIf
Result-shape contract for list()
The terminal list() methods return a documented, stable shape:
| Call | Returns |
|---|---|
query.list() | Vector{PormGRow} |
query.list(:dict) | Vector{Dict{Symbol, Any}} |
query.list(:json) | JSON string |
| `query | > DataFrame` |
Auto-Generated API Docs
The following section contains auto-generated documentation from docstrings in the source code.
Private = false means this section lists only names each module marks as API — exported, or declared public (Julia 1.11+). Internal helpers keep their docstrings in the source but stay off this page (#289). Note Documenter tests Base.ispublic against the module a docstring was written in, not PormG's re-export list, so a user-facing name defined in a submodule needs a public declaration there — see the note above public in src/QueryBuilder.jl.
PormG.install_ai_skills — Function
install_ai_skills(target_dir::String = pwd())Copy PormG's AI skill blueprint into the target project's .github/skills/pormg-usage/ directory. This helps AI assistants (GitHub Copilot, Claude, Cursor, and other agents) provide accurate PormG code suggestions.
The blueprint ships with the PormG package under .github/skills/pormg-usage/ and is a multi-file bundle — SKILL.md plus the supporting reference.md/writing.md it links to. The whole directory is copied so none of SKILL.md's relative links dangle after install.
PormG.setup — Function
setup(path::String = DB_PATH)Interactively setup the connection.yml file for PormG. This will prompt for database adapter, name, and connection details.
PormG.upgrade_guide — Function
upgrade_guide([io::IO = stdout]; from, to = <current code>, structured = false)Print the UPGRADING.md entries a consuming app must work through to move from PormG version from up to to. The default to covers the current code — every released entry plus the uncut ## Unreleased changes the install is running (release-train model), so a consumer dev'ing PormG at HEAD sees work that has not been stamped with a release number yet. Pass to = pkgversion(PormG) to scope to the installed release only. Reads the UPGRADING.md shipped with the resolved PormG install, so the scope is accurate against the version your app actually depends on — not a latest-on-GitHub copy that may not match.
Entries print newest-first; each keeps its "How to find the calls to migrate" grep and its before → after, with the PormG-internal per-app rollout table trimmed off.
from is required — pass the PormG version your app currently depends on. Both from and to accept a VersionNumber or a version string (v"0.2" or "0.2").
Pass structured = true to get the entries back as data instead of printing — a Vector of (; version, title, body) named tuples, newest-first — for programmatic consumers.
Examples
julia> using PormG
julia> PormG.upgrade_guide(from = v"0.1") # everything up to the installed version
julia> PormG.upgrade_guide(from = "0.2", to = "0.3")
julia> entries = PormG.upgrade_guide(from = v"0.1", structured = true);PormG.@pormg_debug — Macro
@pormg_debug
@pormg_debug conditionContributor-facing breakpoint hook, scattered through PormG's source. It expands to nothing, so it costs a package user exactly nothing at runtime.
To make the call sites live while debugging PormG itself, ]dev PormG, load Revise and Infiltrator before PormG, then redefine the macro to expand to a real breakpoint and edit the target file so Revise re-parses it — macros expand at parse time, so redefining the macro without touching the call site has no effect. The step-by-step recipe is in Contributing & Debugging.
@pormg_debug false # inert; flip to `true` (or a real condition) to fire
@pormg_debug model.name == "result" # fires only for that model, once wired upPormG.Kernel — Module
PormG.KernelLayer 1 — the shared vocabulary. Everything in here is a noun: abstract types, constants, the error taxonomy root, and the two dependency-free helpers (_emsg, config) that the rest of the package needs before it can define anything of its own.
The invariant
Kernel imports nothing from PormG. Every other submodule may import from Kernel, and Kernel is included first, so "may I use this name?" stops depending on where a file happens to sit in PormG.jl's include chain.
That ordering used to be implicit, and it bit: the #231 error taxonomy was defined in src/querybuilder/exceptions.jl (included at step 11; that file is now error_funnels.jl and holds only message-composing funnels), so Models, Configuration, Dialect and ConnectionPool — all included earlier — could not name a single one of its types. Each submodule resolves import PormG: … at include time, so "defined later in the module body" means "does not exist yet".
What does NOT belong here
Behavior. In particular Backend.jl stays in PormG, even though it looks like core: the weakdep extensions define their methods as PormG.backend_execute(…) = …, and Julia only accepts a qualified method definition on the module that owns the binding. Moving those generics here would break every extension method with function Kernel.backend_execute must be explicitly imported to be extended — and it would fail at using LibPQ / using SQLite, not at using PormG, so precompiling the package would not catch it. The generics dispatch on the PormGPostgres / PormGSQLite markers defined below, which works fine from where they are.
Rule of thumb: Kernel holds the nouns, PormG keeps the verbs.
PormG.Kernel.BackendCapabilityError — Type
BackendCapabilityError(msg) <: PormGErrorThe active backend cannot do this — a PostgreSQL-only lookup on SQLite (JSONB containment, iunaccent_*), an explicit window frame= on SQLite, bulk_copy on SQLite, with_advisory_lock(...; on_missing_lock = :error) on SQLite, or a SQLite library older than a feature requires. The query is well-formed and the configuration is fine; the remedy is to change the request or the backend — each message names the specific way out. Split out of UnsupportedConnectionError in the pre-publish naming pass — capability limits are a user-facing contract, not an internal error.
PormG.Kernel.ConfigurationError — Type
ConfigurationError <: PormGError (abstract)Umbrella for connection-configuration failures — catch it to get every case below. Like FieldAccessError, this is an abstract mid-node rather than a throwable type, so the pre-existing MissingConfigurationError can live inside the bucket instead of beside it. catch ConfigurationError must not have holes; that class of surprise is the reason this taxonomy exists.
Subtypes: InvalidConfigurationError, WritesDisabledError (the change_data: false write switch — its remedy is a config edit), and Configuration.MissingConfigurationError (a missing folder/connection.yml, or a selected environment with no matching block).
PormG.Kernel.DatabaseError — Type
DatabaseError <: PormGError (abstract)Umbrella for failures raised by the database itself, once a statement has reached it — as opposed to the rest of the taxonomy, which reports misuse of PormG before anything is sent. catch DatabaseError covers every case below without naming a driver package.
Subtypes: IntegrityError (a constraint said no), OperationalError (transient — the connection dropped, a deadlock, a lock timeout), and StatementError (the statement itself was rejected, plus anything the backend could not classify).
All three are built from structured fields rather than a msg::String, so read them with error_message. Each keeps the driver's own exception in .cause, so SQLSTATE-level detail stays available to callers that want it:
try
M.Driver.objects.create("code" => "SEN")
catch e
e isa IntegrityError && return conflict(error_message(e))
e isa OperationalError && return retry()
rethrow()
endConnect-time failure is not here: it never reached a statement, and has been ConnectionPool.PoolConnectError under PoolError since #261.
PormG.Kernel.DefinitionError — Type
DefinitionError <: PormGError (abstract)Umbrella for model-definition-time failures — catch DefinitionError covers both a bad field constructor argument (FieldValidationError) and a bad model/schema shape (ModelDefinitionError). They almost always surface together: one include("models.jl") can raise either, and a handler that names only one silently misses the other — which is exactly what UPGRADING.md's own #239 migration recipe did.
PormG.Kernel.DoesNotExist — Type
DoesNotExist <: PormGErrorget() matched zero rows. Carries model_name and the rendered filters (structured — no msg field; read it with error_message). Often normal control flow: catch it to implement get-or-create-style logic.
PormG.Kernel.FieldAccessError — Type
FieldAccessError <: PormGError (abstract)Umbrella for field/accessor lookup failures — catch it to get both UnknownFieldError ("no such field") and LazyTraversalError ("unsupported lazy traversal").
PormG.Kernel.FieldValidationError — Type
FieldValidationError(msg) <: DefinitionError <: PormGErrorA field constructor was given an invalid argument — a kwarg of the wrong type, a max_length outside its permitted range, a default that does not satisfy the field's own contract, a choices shape that does not parse, or a field type that cannot serve as a primary key.
Raised while defining a model. Contrast InvalidValueError, which is raised while coercing a value on the insert/update path.
PormG.Kernel.FilterError — Type
FilterError(msg) <: PormGErrorAn invalid filter argument/shape, or an operator misused on a JSON/subquery column.
PormG.Kernel.IntegrityError — Type
IntegrityError(adapter, cause) <: DatabaseError <: PormGErrorA constraint rejected the statement — UNIQUE, FOREIGN KEY, NOT NULL, CHECK, or an exclusion constraint. This is the one database failure applications routinely handle rather than propagate, which is why it is its own type.
adapter is "PostgreSQL" or "SQLite"; cause is the driver's own exception. On PostgreSQL this is derived from SQLSTATE class 23, so it is exact; on SQLite it comes from SQLite's own literal constraint messages.
PormG.Kernel.InvalidConfigurationError — Type
InvalidConfigurationError(msg) <: ConfigurationError <: PormGErrorConnection configuration is present but unusable or inconsistent — an unsupported adapter, an unknown connection key, a malformed extensions setting, an unsupported PostgreSQL extension, a model not bound to a connection (or bound to an entry whose pool was never built), a missing driver package (using LibPQ / using SQLite forgotten), or an attempt to overwrite a static connection.
PormG.Kernel.InvalidMigrationError — Type
InvalidMigrationError(msg) <: MigrationError <: PormGErrorThe migration engine refused or could not complete an operation — a duplicate index name in a plan, an invalid answer to an interactive makemigrations prompt, an unimplemented migrate_to(version) path, or a migration-engine step that cannot proceed (no pending plan, an unparseable introspected DDL statement, a missing model file). The importer-pointed-at-the- wrong-backend case is BackendCapabilityError.
PormG.Kernel.InvalidValueError — Type
InvalidValueError(msg) <: PormGErrorA value failed coercion/type validation on insert/update, an identifier failed the fail-closed safety check, or an interval/duration literal could not be parsed. Also raised by the Models.format_*_sql coercion helpers, which the insert/update path calls (#239).
PormG.Kernel.LazyTraversalError — Type
LazyTraversalError(msg) <: FieldAccessError <: PormGErrorAn unprojected ForeignKey was read off a fetched row. PormG has no lazy FK traversal; the message steers the caller to up-front values(...) projection (#204).
PormG.Kernel.MigrationError — Type
MigrationError <: PormGError (abstract)Umbrella for migration-engine failures — catch it to get every case below, including a refused destructive plan.
Subtypes: InvalidMigrationError, and Migrations.DestructiveMigrationError (a destructive plan applied non-interactively without destructive=true).
PormG.Kernel.ModelDefinitionError — Type
ModelDefinitionError(msg) <: DefinitionError <: PormGErrorA model or schema definition is invalid — more than one primary key, a duplicate related_name, an illegal field name, a UniqueConstraint that names an unknown or many-to-many field, an unresolvable ForeignKey / ManyToManyField target, or a Model(...) call given something that is not a PormGField.
PormG.Kernel.MultipleObjectsReturned — Type
MultipleObjectsReturned <: PormGErrorget() matched more than one row — usually a data-integrity surprise rather than control flow. Carries model_name, the offending count, and the rendered filters (structured — no msg field; read it with error_message).
PormG.Kernel.OperationalError — Type
OperationalError(adapter, cause) <: DatabaseError <: PormGErrorThe database could not complete the statement for a reason outside the statement itself, and retrying may succeed — the connection dropped mid-query, a deadlock was detected, a serialization failure occurred, or a lock could not be acquired in time.
catch OperationalError is the retry signal. PormG raises it for with_advisory_lock acquisition timeouts too: contention is a runtime condition, not misuse.
PormG.Kernel.PoolError — Type
PoolError <: PormGErrorAbstract umbrella for connection-pool failures — catch PoolError to handle both saturation and connect failure without naming each. Subtypes: ConnectionPool.PoolTimeoutError (no connection became available in time) and ConnectionPool.PoolConnectError (the backend refused or dropped the connection).
Both concrete types keep their structured fields (adapter, pool_size, attempts, …) and their own showerror, so they do not use the uniform msg::String shape — read them with error_message.
The umbrella lives here rather than in ConnectionPool on purpose (#261). The taxonomy's rule is that a concrete subtype either lives in Kernel or has a dedicated Kernel-owned abstract umbrella above it — the root PormGError does not count, or the rule would be vacuous. The pool errors were the only pair satisfying neither, which is the same mid-include-chain trap that made #239 need the Kernel extraction (#255). Configuration is included before ConnectionPool and already reasons about pool failure, so it could not have named those types.
PormG.Kernel.PormGBackend — Type
PormGBackend <: PormGAbstractTypeThe backend/dialect marker — the dispatch key for SQL rendering and driver selection.
Its two subtypes, PormGPostgres and PormGSQLite, are what every dialect method dispatches on, so a function that renders SQL differently per backend is written as a pair of methods on them rather than as a runtime branch. The values you actually hold are the concrete connection pools: PostgresConnectionPool <: PormGPostgres and SQLiteConnectionPool <: PormGSQLite.
Deliberately not <: PormGSettings: that is the configuration/Settings type, and a pool carries none of its fields (#186).
Adding a backend means defining PormG.backend_* methods in a package extension — see Extending PormG.
PormG.Kernel.PormGBytes — Type
PormGBytes(bytes::Vector{UInt8})A binary payload on its way to the database — the wrapper BinaryField's formatter puts around a byte vector so the parameter collectors can recognize it (#296).
It exists because a bare Vector{UInt8} is indistinguishable from "a list of values", and both backends get that wrong in opposite ways:
- PostgreSQL —
add_parameter!(::PormGPostgresParam, ::AbstractArray)pushes the vector through to LibPQ, which binds every parameter in text format and renders any vector as a PostgreSQL array literal.UInt8[0x00, 0xFF]reaches the server as the five characters{0,255}, andbytea's escape-format input parser accepts that string literally — so the column silently stores the ASCII of{0,255}instead of the two bytes. No error is raised. - SQLite —
add_parameter!(::PormGSQLiteParam, ::AbstractArray)expands an array into one?per element, so an n-byte payload becomes n placeholders and the statement fails on a column-count mismatch.
Dispatching on the wrapper instead of on Vector{UInt8} keeps filter("x__in" => UInt8[1, 2]) expanding into an IN list as it always has — only values that came through a binary field are treated as one opaque blob.
Layer 1 on purpose: Models produces it and QueryBuilder consumes it, so neither can own it.
PormG.Kernel.PormGError — Type
PormGError <: ExceptionRoot of PormG's semantic error taxonomy (#231). Every error PormG raises for a domain failure — an unknown field, an invalid value, a refused write, a pool timeout, a database rejection — is a subtype, so one catch clause covers the whole surface:
try
M.Driver.objects.get("driverref" => "nobody")
catch e
e isa PormGError || rethrow()
@error "query failed" msg=error_message(e)
endUse error_message to read a caught error: the structured subtypes carry typed fields rather than a .msg string.
Catch a narrower type when you want to act on one failure — DoesNotExist, IntegrityError, PoolTimeoutError — and the umbrellas (FieldAccessError, PoolError, DatabaseError, ConfigurationError, MigrationError, DefinitionError) to group a family.
These are deliberately not <: ArgumentError: a clean break so callers match a type instead of string-matching a message. Plain Julia-level misuse (a missing kwarg, a missing path) still raises the stock Julia exception, because it is not a PormG domain error.
The type lives in PormG.Kernel — layer 1 — so every submodule can name it and its subtypes regardless of include order (#239). The full list is in the API reference.
PormG.Kernel.PormGField — Type
PormGField <: PormGAbstractTypeBase type for field definitions — CharField, IntegerField, ForeignKey, and the rest.
A field is a component of a model, not a kind of model: PormGField is a sibling of PormGModel, not a subtype, so it deliberately does not satisfy ::PormGModel signatures, which all read model-only attributes (#186).
Fields own their validation — a value is checked here, before any SQL is generated — and carry the formatter that coerces a Julia value on its way into the database (inserts, bulk writes, bound filter parameters). Values read back are not decoded through the field type.
See also PormG Field Types Reference.
PormG.Kernel.PormGModel — Type
PormGModel <: PormGAbstractTypeBase type for model definitions — one table, its fields, and its relations.
Instances are produced by Models.Model("table_name", field = FieldType(...), ...) and collected by @import_models / set_models. Dispatch on it when you are writing code that takes "any model", such as a framework extension that registers its own tables.
See also PormGField, Defining Models in PormG, Extending PormG.
PormG.Kernel.ProtectedError — Type
ProtectedError(msg) <: PormGErrorA delete() was refused because other rows reference the target through a ForeignKey declared with on_delete = PROTECT (or RESTRICT). Nothing about the call is malformed — the data forbids it, and the remedy is to delete or reassign the referencing rows first. Mirrors Django's ProtectedError/RestrictedError; previously filed under the long-tail QueryBuildError, which made this case indistinguishable from a malformed delete.
PormG.Kernel.QueryBuildError — Type
QueryBuildError(msg) <: PormGErrorStructural/API misuse while building a query — joins, CTEs, projection, ordering, window and bulk configuration, and the like. The default bucket for query-builder misuse that isn't one of the sharper categories below.
PormG.Kernel.SQLObject — Type
SQLObject <: PormGAbstractTypeBase type for the query state itself — the accumulated filters, projections, joins and ordering that a query is built from.
The concrete type you hold is SQLObjectQuery. You rarely name SQLObject in application code; it appears when a helper accepts "the underlying query object", e.g. the mq.object handed to a subquery or CTE constructor.
See also SQLObjectHandler, Architecture & Request Flow.
PormG.Kernel.SQLObjectHandler — Type
SQLObjectHandler <: SQLObjectBase type for the chainable wrapper around a query — the thing Model.objects returns and .filter(...), .values(...), .list() hang off.
The concrete type is ObjectHandler; its fluent methods are synthesized by getproperty, so they have no bindings of their own and ?query.filter cannot work — the full method reference lives on object instead (?object).
See also SQLObject (the state it wraps).
PormG.Kernel.StatementError — Type
StatementError(adapter, cause) <: DatabaseError <: PormGErrorA statement failed to execute — invalid SQL, an unknown table or column, a type the backend would not accept, or insufficient privileges. Also the landing type for any failure on the database path that could not be classified, so catch DatabaseError never has a hole.
Usually a bug to fix rather than a condition to handle. The driver's exception is in .cause; the SQL text is deliberately not stored, because it can embed user data (the @error … sql=… log sites already surface the statement where that is appropriate).
The wording says could not execute, not the database rejected this, on purpose. Being the unclassified fallback means a PormG-internal fault on the statement path can land here too — the SQLite worker's malformed-payload invariant, for one — and claiming the server refused something it never saw would send a reader hunting for a SQL bug that does not exist.
PormG.Kernel.TransactionError — Type
TransactionError(msg) <: PormGErrorThe transaction API was used in a way that cannot work — atomic(durable=true) nested inside an open transaction, or an operation on a model bound to one connection attempted while a transaction is open on another.
Not a DatabaseError: nothing was sent, and the database is not involved. Both cases are caught before any statement is issued. A deadlock or a rollback the server forces is an OperationalError instead.
Introduced in #268 so the two checks stop reporting as unrelated types (QueryBuildError said "query shape" for what is a transaction-nesting mistake; InvalidConfigurationError said "your config is wrong" when the config was fine and the call pattern was not).
PormG.Kernel.UnknownFieldError — Type
UnknownFieldError(msg) <: FieldAccessError <: PormGErrorA field, alias, column, or __ lookup path does not exist on the model or the projected row.
PormG.Kernel.UnsafeMutationError — Type
UnsafeMutationError(msg) <: PormGErrorAn UPDATE or DELETE was requested without a filter (or in another unsafe shape) and refused.
PormG.Kernel.UnsupportedConnectionError — Type
UnsupportedConnectionError(msg) <: PormGErrorA connection object that is neither a PostgreSQL nor a SQLite pool reached an execution path — a PormG internal dispatch bug; the message asks the user to report it. The catchable replacement for the internal ErrorException from #197.
Narrowed in the pre-publish naming pass: it previously also covered backend capability limits (now BackendCapabilityError) and models not bound to a connection (now InvalidConfigurationError, whose docstring always claimed that case) — three disjoint remedies distinguishable only by message text, which is the failure mode this taxonomy exists to remove.
PormG.Kernel.WritesDisabledError — Type
WritesDisabledError(msg) <: ConfigurationError <: PormGErrorThe connection is not permitted to insert/update/delete — its settings carry change_data: false. The remedy is a configuration edit (connection.yml), which is why this lives under ConfigurationError. Renamed from PermissionError in the pre-publish naming pass: that name read as OS/file permissions to some audiences and database GRANTs to others, while the actual meaning is PormG's own write switch.
PormG.Kernel.error_message — Method
error_message(e::PormGError) -> StringThe text of any PormG error, as a String.
Use this instead of e.msg. Seven subtypes are built from structured fields and have no msg field at all — DoesNotExist, MultipleObjectsReturned, ConnectionPool.PoolTimeoutError, ConnectionPool.PoolConnectError, and the three DatabaseError subtypes (IntegrityError, OperationalError, StatementError) — so e.msg throws a FieldError on exactly the errors a caller is least likely to have tested against (#261, #268).
try
M.Result.objects.values("bad alias!" => "points").list()
catch e
e isa PormGError || rethrow()
@error "PormG rejected the query" msg=error_message(e) type=typeof(e)
endDefined via showerror, which every subtype implements, so it stays correct for subtypes added later without needing a new method. For subtypes that use the generic showerror above, the result is exactly e.msg (it prints that field verbatim, and _emsg has already normalized any ANSI at construction). Subtypes with their own showerror return that richer rendering instead — e.g. Configuration.MissingConfigurationError and Migrations.DestructiveMigrationError both carry a msg yet prefix it with the error name, so error_message is a superset of .msg, never a subset.
PormG.Kernel.register_ignore_tables! — Method
register_ignore_tables!(tables) -> Vector{String}Register table-name patterns that schema introspection (convert_schema_to_models, import_models_from_*, makemigrations) should always skip — e.g. a consumer framework's own infrastructure tables. Additive and idempotent (deduplicated); returns the full list.
Intended to be called once at load time, typically from a package extension's __init__:
# in YourPkgPormGExt.__init__
isdefined(PormG, :register_ignore_tables!) && PormG.register_ignore_tables!(["yourpkg_jobs"])PormG.Functions — Module
PormG.FunctionsThe SQL function library, and the index of it. Because these names are not exported into Main by using PormG, ?Sum answers nothing until you import them — so this docstring is the entry point: it lists every constructor and where each family is documented.
Aggregate — Sum, Avg, Count, Max, Min — see Filters and Aggregates
Conditional — Case, When — see Functions and Dates
Window — WindowOver, WindowSpec, Rank, DenseRank, RowNumber, Lag, Lead, FirstValue, LastValue, NthValue — see Window Functions
String — Concat, Lower, Upper, Length, Replace, Trim, LTrim, RTrim
Math — Abs, Round, Floor, Ceil, Sqrt, Exp, Ln, Power, Mod
Type / value — Cast, Extract, ToChar, Value, Coalesce, Greatest, Least, NullIf
They live here rather than at the top level because the names are generic enough to collide with Base and user code (Sum, Count, Max, Replace, Round, Length…), so the library has exactly one home and you opt in explicitly:
using PormG, PormG.Functions # brings Sum, Count, … into scope
using PormG.Functions: Sum, Count # …or just the ones you use
# or qualify without importing:
M.Result.objects.values("n" => PormG.Functions.Count("resultid"))Q, Qor, F, Exists, OuterRef, Subquery, CTE and Interval are not part of this library — they are query primitives and stay on the top-level using PormG surface.
DataFrames.DataFrame — Method
Creates a DataFrame directly from a SQLObjectHandler query.
This extends the DataFrame constructor to work directly with PormG query objects,
Arguments
objct::SQLObjectHandler: The SQL object handler containing the query
Returns
DataFrames.DataFrame: The query results as a DataFrame
Example
query = M.Result |> object
query.filter("raceid__year" => 2020)
query.values("driverid__forename", "constructorid__name", "laps")
df = query |> DataFrame # Direct conversion to DataFramePormG.QueryBuilder.Interval — Type
Interval(period)
Interval(duration_string)Explicit duration wrapper for F-expression date arithmetic (#25). Holds a Julia Dates.Period / Dates.CompoundPeriod, or parses a portable time-duration string ("HH:MM:SS(.fff)", "M:SS", or bare seconds) into a time-only CompoundPeriod.
Interval(...) is interchangeable with a bare period wherever date arithmetic is used — F("date") + Interval(Month(1)) is identical to F("date") + Month(1). The string form is the escape hatch for time-based intervals: F("logged_at") + Interval("01:30:00").
Note: the name is shared with the Intervals.jl ecosystem — if you also using Intervals, disambiguate as PormG.QueryBuilder.Interval.
PormG.QueryBuilder.ObjectHandler — Type
ObjectHandler <: SQLObjectHandlerThe query handler Model.objects returns — the object every fluent chain is built on.
Its methods are synthesized by getproperty rather than being real fields, which means the Julia REPL cannot help you with them: ?query.filter does not work (it errors, for any Julia value). The complete fluent reference lives on object — type ?object — and on the API reference.
query = M.Driver.objects # an ObjectHandler
query.filter("nationality" => "Brazilian")
rows = query.values("forename", "surname").list()Chainable methods mutate the handler and return it; terminal methods execute and return a result. Use .copy() when you need to branch a chain without disturbing the original.
PormG.QueryBuilder.PormGRow — Type
A model-aware row returned by list(), first(), and get().
Wraps a Dict{Symbol, Any} and remembers which model produced it, enabling dot-access to fields and many-to-many relationship accessors.
PormG.QueryBuilder.WindowSpec — Type
WindowSpec <: SQLTypeThe OVER (...) clause of a window function, in structured form.
Fields
partition_by::Vector— the grouping the window restarts on. Empty means one window over the whole result set.order_by::Vector— the ordering inside each window, stored as given: a"-points"entry stays"-points", and the-prefix is resolved toDESCat build time.frame::Union{String,Nothing}— an explicit frame clause, ornothingfor the SQL default (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). PostgreSQL only.
Build one with WindowOver, which validates and coerces its arguments; the @kwdef constructor is exported for the rare case where you want to assemble or mutate a spec directly. The same spec can be reused across several window functions in one query.
See also Window Functions.
Base.first — Method
first(objct::SQLObjectHandler; show_query::Symbol = :execute)Return the first PormGRow matching the current query, or nothing if no records match.
Like every read terminal (count, exists, list, get), first executes on an internal copy of the handler — the limit(1) it needs is applied to that copy, never to objct. The handler is reusable afterwards, including for .update():
q = M.Driver.objects
q.filter("nationality" => "British")
driver = q.first() # q is unchanged — no limit leaks in
q.update("nationality" => "English") # still valid on the same handlerBase.get — Method
get(objct::SQLObjectHandler, filters...; show_query=:execute) -> PormGRowReturn exactly one row matching the query filters.
Filters can be passed inline or applied with .filter() before calling .get():
driver = M.Driver.objects.get("driverref" => "hamilton")
driver = M.Driver.objects.filter("driverref" => "hamilton").get()Raises DoesNotExist when no rows match and MultipleObjectsReturned when more than one row matches.
Like every read terminal, get executes on an internal copy of the handler: inline filters do not persist on objct, so the handler can be reused afterwards with its original filter list intact.
Base.last — Method
last(objct::SQLObjectHandler; show_query::Symbol = :execute)Return the last PormGRow matching the current query, or nothing if no records match.
The mirror of first: it inverts the query's ordering and takes one row. When an order_by(...) is set, last() returns the row that first() would return under the reversed ordering. When no ordering is set, it falls back to primary-key descending, so last() is always well-defined (matching Django). Like every read terminal, it runs on an internal copy — the inverted ordering and limit(1) never leak into the caller's handler.
PormG.QueryBuilder.Abs — Method
Abs(column)Returns the absolute value of a number.
PormG.QueryBuilder.Avg — Method
Avg(x; distinct::Bool = false)Aggregate AVG(x) — the mean of x across the group.
x is a field path ("points", "driverid__surname"), an F expression, or a nested function object. With distinct = true it renders AVG(DISTINCT x).
Like Count and Sum — and unlike Max/Min — AVG is covered by the to-many fan-out guard (#74): a join that multiplies rows would silently inflate the mean, so PormG raises instead. Passing distinct = true is an explicit opt-in and is exempt.
See also Filters and Aggregates.
PormG.QueryBuilder.CTE — Method
CTE(name::AbstractString, path::AbstractString; desc::Bool = false) -> CTEReferenceReference a column of a CTE declared with .with(...). The CTE's columns live in their own namespace, separate from the model's field paths, so a CTE may legally share a name with a field and neither shadows the other (#444):
parent_cte = M.Cj_parent.objects
parent_cte.values("id", "sku")
q = M.Cj_child.objects
q.with("parent" => parent_cte) # "parent" is ALSO a ForeignKey of Cj_child
q.values("note",
"parent__sku", # the ForeignKey's column — unambiguous
"fk" => CTE("parent", "sku")) # the CTE's column — unambiguousThe second argument is a path, not a bare column, and carries the same __ vocabulary the rest of PormG uses — a hop out of the CTE through a projected ForeignKey, a JSON sub-path, or an operator suffix:
q.filter(CTE("ev", "sku") => "ABC") # plain column
q.values("s" => CTE("ev", "parent__sku")) # hop through a projected FK
q.filter(CTE("ev", "meta__driver") => "senna") # JSON sub-path
q.filter(CTE("ev", "seen__@yyyy_mm__@lte") => "1991-10") # operator suffix
q.filter("raceid" => CTE("r91", "raceid")) # correlate an unkeyed CTE (#44)
q.order_by(CTE("monaco_stats", "total_points"; desc = true))An unaliased projection is named by joining the two with a double underscore — values(CTE("parent", "sku")) emits the output column parent__sku.
SQL functions, aggregates and window clauses accept a handle wherever they accept a field path — Lower(CTE("ev","sku")), Cast(CTE("ev","qty"), "text"), Sum(CTE("ev","qty")), Rank(over = WindowOver(partition_by = CTE("ev","sku"))).
desc = true is meaningful in order_by(...) and in a window's order_by; anywhere else it raises a QueryBuildError. A CTE column cannot be referenced from on(...), cjoin(...) or cjoin_on(...) — those clauses target model relations — however it is spelled, including as the operand of an F comparison.
See also Subqueries and CTEs.
PormG.QueryBuilder.Case — Method
Case(conditions; default = "NULL", output_field = nothing)A SQL CASE … END expression: evaluate each When branch in order and return the first match.
Arguments
conditions: aVectorofWhenbranches, or a single bareWhen.default: theELSEbranch. Defaults to the string"NULL", which is emitted as the SQL literalNULL— it is not a bound parameter, so pass a Julia value (0,"") when you want a real default.output_field: the result type. Accepts aPormGFieldinstance (e.g.CharField(), whose.typeis used) or a raw SQL type string. Renders as a::typecast on PostgreSQL and aCAST(...)on SQLite.
Usable anywhere a column expression is — in values(), nested inside Sum, as a filter right-hand side, and in .update().
using PormG.Functions: Case, When
using PormG.Models: CharField # field types are not part of PormG.Functions
"podium" => Case([
When("positionorder" => 1, then = "win"),
When("positionorder__@lte" => 3, then = "podium"),
], default = "none", output_field = CharField())See also When, Functions and Dates.
PormG.QueryBuilder.Cast — Method
Cast(expression, type)Casts a column or expression to a specific SQL type.
PormG.QueryBuilder.Ceil — Method
Ceil(column)Returns the smallest integer greater than or equal to a number.
PormG.QueryBuilder.Coalesce — Method
Coalesce(args...; output_field=nothing)Returns the first non-null value in the list of arguments.
PormG.QueryBuilder.Concat — Method
Concat(expressions; output_field=nothing)Concatenates multiple strings or columns.
PormG.QueryBuilder.Count — Method
Count(x; distinct::Bool = false)
Creates an aggregate COUNT function object for use in query building.
Arguments
x: The column or expression to count.distinct::Bool = false: Iftrue, counts only distinct values ofx.
Examples
# Count just when other_model_id is distinct
query = MyModels.model_test |> object;
query.filter("id__@gte" => 1)
query.values("id", "count" => Count("other_model_id", distinct=true))
df = query |> DataFramePormG.QueryBuilder.DenseRank — Method
DenseRank(; over::WindowSpec = WindowOver())
DenseRank(over::WindowSpec)Window DENSE_RANK() — like Rank, but without gaps after ties: two rows tied for 1st are both 1 and the next row is 2, not 3.
Use it when you want "how many distinct values outrank this one", and Rank when you want a true finishing position.
See also RowNumber, Window Functions.
PormG.QueryBuilder.Exists — Method
Exists(query::SQLObjectHandler) -> ExistsObjectWrap a subquery as a SQL EXISTS predicate. Renders as EXISTS (SELECT 1 … LIMIT 1), so it answers "is there at least one match?" without counting or fetching the child rows.
Correlate the subquery to the current outer row with OuterRef; the result can be used two ways.
As a filter predicate — pass it positionally to filter, alongside ordinary pairs or inside Qor:
# Results whose driver set a lap under 90 s in that same race
fast_laps = M.Lap_times.objects.filter(
"raceid" => OuterRef("raceid"),
"driverid" => OuterRef("driverid"),
"milliseconds__@lte" => 90_000,
)
n = M.Result.objects.filter(Exists(fast_laps)).count()As a projected boolean column — pair it with an alias inside values:
standings = M.Driver_standings.objects.filter("driverid" => OuterRef("driverid"))
query = M.Driver.objects
query.values("surname", "has_standings" => Exists(standings))SQLite returns 0/1 integers for a projected Exists; PostgreSQL returns booleans.
See also Subquery for the scalar (single-value) form and Subqueries and CTEs.
PormG.QueryBuilder.Exp — Method
Exp(column)Returns the exponential value (e^x) of a number.
PormG.QueryBuilder.Extract — Method
Extract(column, part)Extracts a component (YEAR, MONTH, DAY, etc.) from a date/time column.
PormG.QueryBuilder.F — Method
F(field_name::String) -> FExpressionReference a database column rather than a Julia value (the Django F() equivalent). The comparison or arithmetic happens inside SQL, so no data is pulled into Julia and the update stays a single atomic statement.
F expressions support +, -, * and / against constants, other Fs and SQL functions. + and - additionally accept a Dates period or an Interval, for date arithmetic (* and / do not).
# Field-to-field comparison — grid position worse than finishing position
query = M.Result.objects.filter(F("grid") > F("positionorder"))
# Arithmetic projection, computed by the database
query = M.Result.objects
query.values("resultid", "adjusted" => F("points") * 2)
# Atomic update against the column's current value (no read-modify-write race)
M.Result.objects.filter("resultid" => 1).update("points" => F("points") + 1)
# Field-to-field update
M.Result.objects.filter("resultid" => 1).update("position" => F("positionorder"))Prefer a plain lookup when the predicate compares against a scalar: write filter("points__@gt" => 20), not filter(F("points") > 20).
See also Field Expressions.
PormG.QueryBuilder.FirstValue — Method
FirstValue(x; over::WindowSpec = WindowOver())Window FIRST_VALUE(x) — the value of x in the first row of the window frame.
Safe under the default frame (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), because the frame always starts at the partition's first row. LastValue is not — see its docstring.
See also NthValue, Window Functions.
PormG.QueryBuilder.Floor — Method
Floor(column)Returns the largest integer less than or equal to a number.
PormG.QueryBuilder.Greatest — Method
Greatest(args...; output_field=nothing)Returns the greatest value in the list of arguments.
PormG.QueryBuilder.LTrim — Method
LTrim(column)Removes leading whitespace from a string.
PormG.QueryBuilder.Lag — Method
Lag(x; offset::Integer = 1, default = nothing, over::WindowSpec = WindowOver())Window LAG(x, offset) — the value of x from offset rows earlier in the window.
Arguments
x: the column to read. Required — passingnothingraisesQueryBuildError.offset: how many rows back. Must be non-negative; negatives raiseQueryBuildError(useLeadto look forward).default: value returned at the window edge where no previous row exists. Omit it and those rows come backmissing/NULL.over: theWindowOverspec.order_byis what makes "earlier" meaningful.
offset and default are bound as query parameters, not interpolated.
See also Lead, Window Functions.
PormG.QueryBuilder.LastValue — Method
LastValue(x; over::WindowSpec = WindowOver())Window LAST_VALUE(x) — the value of x in the last row of the window frame.
SQL's default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, so with an order_by and no explicit frame the "last visible row" is the current row — LastValue silently returns each row's own value instead of the partition's last. This is correct SQL, not a PormG bug, and it is the single most common window-function trap.
Pass an explicit frame to see the whole partition:
WindowOver(
partition_by = ["constructorid"],
order_by = ["positionorder"],
frame = "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING" # PostgreSQL only
)frame is PostgreSQL-only (BackendCapabilityError on SQLite). On SQLite, drop the order_by so the whole partition is one frame, or compute the value another way.
See also FirstValue, Window Functions.
PormG.QueryBuilder.Lead — Method
Lead(x; offset::Integer = 1, default = nothing, over::WindowSpec = WindowOver())Window LEAD(x, offset) — the value of x from offset rows later in the window. The forward-looking mirror of Lag; the arguments, the parameter binding, the QueryBuildError on a negative offset, and the default-at-the-edge behavior are identical.
See also Window Functions.
PormG.QueryBuilder.Least — Method
Least(args...; output_field=nothing)Returns the least value in the list of arguments.
PormG.QueryBuilder.Length — Method
Length(column)Returns the length of a string.
PormG.QueryBuilder.Ln — Method
Ln(column)Returns the natural logarithm of a number.
PormG.QueryBuilder.Lower — Method
Lower(column)Converts a string to lowercase.
PormG.QueryBuilder.Max — Method
Max(x)Aggregate MAX(x) — the largest value of x in the group.
There is no distinct keyword: MAX(DISTINCT x) and MAX(x) are the same value.
MAX/MIN are deliberately exempt from the to-many fan-out guard (#74) that Count, Sum and Avg trip: duplicating rows across a to-many join cannot change an extremum, so the query is safe where a sum would be wrong.
The value comes back in whatever form the backend returns for that column — an aggregate is not decoded through the model field's type, so a MAX over a DateField is the driver's representation (a String on SQLite), not a Date. Convert it yourself if you need one.
See also Min, Filters and Aggregates.
PormG.QueryBuilder.Min — Method
Min(x)Aggregate MIN(x) — the smallest value of x in the group. The mirror of Max in every respect: no distinct keyword, exempt from the fan-out guard (#74), and the result is the backend's own representation rather than the model field's type.
See also Filters and Aggregates.
PormG.QueryBuilder.Mod — Method
Mod(dividend, divisor)Returns the remainder (modulo) of a division.
PormG.QueryBuilder.NthValue — Method
NthValue(x, n::Integer; over::WindowSpec = WindowOver())Window NTH_VALUE(x, n) — the value of x in the n-th row of the window frame, counting from 1. n <= 0 raises QueryBuildError.
n is positional, not a keyword, and is rendered as a literal integer in the SQL rather than a bound parameter — SQL requires a constant there.
The same frame caveat as LastValue applies whenever n reaches past the current row: under the default frame those rows come back NULL.
using PormG.Functions: NthValue, WindowOver
"runner_up" => NthValue("driverid__surname", 2,
over=WindowOver(partition_by=["raceid"], order_by=["positionorder"]))See also FirstValue, Window Functions.
PormG.QueryBuilder.NullIf — Method
NullIf(field1, field2)Returns NULL if field1 equals field2, otherwise returns field1.
PormG.QueryBuilder.OuterRef — Method
OuterRef(field_name::AbstractString) -> OuterRefObjectReference a column of the enclosing query from inside a subquery — the correlation that turns an independent child query into a per-outer-row one. Use it inside the query you hand to Exists or Subquery.
# "did this driver set a lap under 90 s in this race?" — both columns come from the outer row
fast_laps = M.Lap_times.objects.filter(
"raceid" => OuterRef("raceid"),
"driverid" => OuterRef("driverid"),
"milliseconds__@lte" => 90_000,
)
query = M.Result.objects.filter(Exists(fast_laps))OuterRef("pk") resolves to the outer model's primary key, so a correlation does not have to name the column: filter("driverid" => OuterRef("pk")) against an outer M.Driver query.
Two limits, both enforced with a QueryBuildError:
- One level only. It binds to the immediately enclosing query, so a projected subquery nested inside another projected subquery is rejected rather than silently correlated to the wrong level.
- Correlated context required. Used outside an
Exists/Subquerybuild there is no outer query to bind to.
Correlate on a base column of the outer model. A joined path (OuterRef("constructorid__name")) adds a join to the outer query and is outside the validated surface.
PormG.QueryBuilder.Power — Method
Power(base, exponent)Returns base raised to the power of exponent.
PormG.QueryBuilder.Q — Method
Q(x...)Create a QObject with the given filters.
Arguments
x...: key-value pairs,Qor(x...), orQ(x...)objects.
Example
a = object("tb_user")
a.filter(Q("name" => "John", Qor("age" => 18, "age" => 19)))PormG.QueryBuilder.Qor — Method
Qor(x...)Create a QorObject from the given arguments. The QorObject represents a disjunction of SQLTypeQ or SQLTypeQor objects.
Arguments
x...: A variable number of arguments. Each argument can be either aSQLTypeQorSQLTypeQorobject, or aPairobject.
Example
a = object("tb_user")
a.filter(Qor("name" => "John", Q("age__gte" => 18, "age__lte" => 19)))PormG.QueryBuilder.RTrim — Method
RTrim(column)Removes trailing whitespace from a string.
PormG.QueryBuilder.Rank — Method
Rank(; over::WindowSpec = WindowOver())
Rank(over::WindowSpec)Window RANK() — position within the window, leaving gaps after ties: two rows tied for 1st are both 1 and the next row is 3.
Takes no column; the ordering comes entirely from over. Omitting over ranks the whole result set as one unordered window, which is rarely what you want — pass a WindowOver with order_by. The positional form Rank(spec) is shorthand for Rank(over=spec).
See also DenseRank (no gaps), RowNumber (always unique), Window Functions.
PormG.QueryBuilder.Replace — Method
Replace(column, find, replace)Replaces all occurrences of find with replace in the string.
PormG.QueryBuilder.Round — Function
Round(column, precision=0)Rounds a number to the specified precision.
PormG.QueryBuilder.RowNumber — Method
RowNumber(; over::WindowSpec = WindowOver())
RowNumber(over::WindowSpec)Window ROW_NUMBER() — a unique sequential number per row within the window, starting at 1.
Unlike Rank and DenseRank it never repeats a value, which means tied rows get an arbitrary order between them. If the numbering has to be reproducible, add a tiebreaker column to the order_by of the WindowOver.
See also Window Functions.
PormG.QueryBuilder.Sqrt — Method
Sqrt(column)Returns the square root of a number.
PormG.QueryBuilder.Subquery — Method
Subquery(query::SQLObjectHandler) -> SubqueryObjectProject a scalar correlated subquery as a column of the enclosing SELECT (#92) — one value per outer row, computed by its own sub-SELECT.
The inner query must select exactly one column, and it correlates to the outer row through OuterRef. Always project it with an alias — a bare Subquery(...) inside values raises.
# How many standings rows each driver has — one exact count per driver
standings = M.Driver_standings.objects
standings.filter("driverid" => OuterRef("driverid"))
standings.values("t" => Count("driverstandingsid"))
query = M.Driver.objects
query.values("surname", "total_standings" => Subquery(standings))
df = query |> DataFrameThis is the fan-out-safe way to aggregate across a to-many relation: two Subquery columns over two different relations stay exact, where a joined values(Count(...), Count(...)) would row-multiply (the guard for that is #74).
Combining a correlated Subquery with an outer aggregate is only well-defined when the correlated column is itself grouped. If it is not, PostgreSQL fails loudly while SQLite silently evaluates the subquery against an arbitrary row of each group. See Subqueries and CTEs.
See also Exists for the boolean form and OuterRef for the correlation.
PormG.QueryBuilder.Sum — Method
Sum(column; distinct=false)Computes the sum of all values in the column.
PormG.QueryBuilder.ToChar — Method
ToChar(x, format::String; formatter = nothing)Format a date/time column as text — PostgreSQL to_char(x, format), SQLite strftime.
Arguments
x: a field path,Fexpression, function object, or a vector of field paths.format: a PostgreSQLto_charpattern, e.g."YYYY-MM","YYYY-MM-DD","YYYY".formatter: an optional Julia-side hook applied to the returned values. Accepts aFunction, or aPormGFieldwhose.formatteris used.
On SQLite the pattern is translated through PormG's strftime map rather than passed through, so only the patterns in that map work. The common date buckets ("YYYY", "YYYY-MM", "YYYY-MM-DD") are portable; exotic to_char patterns are PostgreSQL-only.
using PormG.Functions: ToChar, Count
# Races per month
query.values("month" => ToChar("date", "YYYY-MM"), "n" => Count("raceid"))Named ToChar since 0.3.0 (previously To_char, with a formater keyword).
See also Functions and Dates.
PormG.QueryBuilder.Trim — Method
Trim(column)Removes leading and trailing whitespace from a string.
PormG.QueryBuilder.Upper — Method
Upper(column)Converts a string to uppercase.
PormG.QueryBuilder.Value — Method
Value(x)Wraps a literal value (String, Number, or Nothing) for use in SQL queries.
PormG.QueryBuilder.When — Method
When(condition; then = 0, otherwise = missing)One WHEN condition THEN value branch of a SQL CASE.
condition accepts four forms:
- a lookup pair —
When("points__@gt" => 10, then = 1) - a tuple of pairs, ANDed together —
When(("points__@gt" => 10, "grid" => 1), then = 1) - a
Q(...)/Qor(...)object, for OR and nested boolean logic - an operator or function object, e.g. an
Fcomparison
then defaults to 0. Both then and the CASE ELSE value are bound as query parameters.
Passing otherwise wraps the branch in a complete CASE … ELSE … END, so a two-way conditional needs no Case at all:
# Points scored, or 0 for a non-points finish — one call, no Case needed.
"scored" => When("points__@gt" => 0, then = 1, otherwise = 0)Inside Case([...]), leave otherwise unset — Case owns the ELSE branch.
See also Case, Functions and Dates.
PormG.QueryBuilder.WindowOver — Function
WindowOver(partition_by, order_by = []; frame = nothing) -> WindowSpec
WindowOver(; partition_by = [], order_by = [], frame = nothing) -> WindowSpecBuild the OVER (...) clause shared by every window function — this is the constructor you want; WindowSpec is the value it returns.
Arguments
partition_by: restart the window per group. A field path, anFexpression, or a vector/tuple of them.Symbols are accepted and converted.order_by: ordering inside each window. Strings use the repo-wide"-field"convention forDESC;SQLOrderobjects also work.frame: a raw frame clause such as"ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING".
Both list arguments accept a bare scalar, so partition_by = "raceid" and partition_by = ["raceid"] are equivalent. An entry of any other type raises QueryBuildError.
Passing frame on a SQLite connection raises BackendCapabilityError. Everything else here works on both backends (SQLite ≥ 3.25.0).
using PormG.Functions: WindowOver, Rank
# Rank drivers within each race — the ranking restarts per race.
query = M.Driver_standings.objects
query.filter("raceid__@in" => [305, 306], "points__@gt" => 0)
query.values(
"raceid", "driverid__surname", "points",
"race_rank" => Rank(over=WindowOver(
partition_by=["raceid"], # restart the ranking for each race
order_by=["-points"] # highest points = rank 1
))
)See also Window Functions.
PormG.QueryBuilder.allocate_primary_keys — Method
allocate_primary_keys(objct::SQLObjectHandler, df::DataFrame; clone=true) -> DataFramePre-allocate sequential primary key values for rows in df that are missing an auto-generated primary key, and return the DataFrame with the pk column populated.
Use this when you need the assigned ids before inserting—for example, to wire up foreign key columns in related tables that must be bulk-inserted in the same transaction.
If df already contains the primary key column with at least one non-blank value it is returned unchanged and no ids are reserved. If the column is absent, or every value in it is blank (missing, nothing, or an empty string), ids are reserved from the database.
If the column contains mixed values—some rows have explicit pk values and some are blank—a @warn is emitted and the DataFrame is still returned unchanged. The blank rows are left as-is and will raise a QueryBuildError when bulk_insert is called. To resolve this you can: (1) provide a pk value for every row, (2) remove the pk column so all ids are allocated automatically, or (3) pre-fill the blank rows before calling this function.
PostgreSQL
Uses nextval(pg_get_serial_sequence(...)) to atomically consume N values from the identity/serial sequence. The reserved ids are guaranteed not to collide with concurrent inserts. Note that if the subsequent bulk insert is never executed (e.g. the transaction is rolled back), the consumed sequence values are not returned—this is normal PostgreSQL sequence behaviour; gaps are harmless.
SQLite
Reads the starting point from max(MAX(pk), sqlite_sequence.seq), assigns the next N ids from there, and bumps the table's sqlite_sequence counter to the end of the reserved range. That keeps both later autoincrement inserts and later allocate_primary_keys() calls from reusing ids that were reserved but not yet inserted. This read-then-write is self-protecting: if it is not already running inside a transaction on this connection it auto-opens one (run_in_transaction, i.e. BEGIN IMMEDIATE + the in-process write lock) so no concurrent writer can claim the same range in between — mirroring bulk_insert/bulk_copy/bulk_update. Wrapping the whole pre-allocation and insert together in run_in_transaction is still recommended: then a rolled-back insert also releases the reserved ids, whereas a standalone allocation whose later insert fails durably burns its range (a harmless gap, exactly like PostgreSQL).
Arguments
objct: ASQLObjectHandler(typicallyM.Model.objects). Only the underlying model is consulted — any filters, ordering, or annotations attached to the handler are ignored, since pk allocation is a table-level operation independent of any query.df: TheDataFramethat will be bulk-inserted.clone::Bool = true: Whentrue(default) the returned DataFrame is a genuine copy with independent column vectors — the new pk column exists only on the returned frame, the caller's DataFrame is left untouched, and element writes on either frame never reach the other. Set tofalseto write the new pk column into the caller's DataFrame in place and skip the copy.
Notes
- The returned pk column is a plain
Vector{Int}. If the input DataFrame had aVector{Union{Missing,Int}}pk column, the missing-able element type is dropped after allocation. - The PostgreSQL backend currently assumes the model lives in the default search path (typically
public). Models in other schemas are not supported by this helper. (_update_sequenceno longer shares the limitation: since #344 it resolves the sequence through the table's ownrelnamespace.)
Example
# Allocate driver ids before building the results table
drivers_df = CSV.File("f1/drivers.csv") |> DataFrame
PormG.run_in_transaction("db_2") do
drivers_df = allocate_primary_keys(M.Driver.objects, drivers_df)
results_df = DataFrame(
driverid = repeat(drivers_df.driverid, inner=10),
raceid = ...,
...
)
bulk_insert(M.Driver.objects, drivers_df)
bulk_insert(M.Result.objects, results_df)
endPormG.QueryBuilder.bulk_copy — Method
bulk_copy(objct::SQLObjectHandler, df_o::DataFrames.DataFrame; kwargs...)Performs a high-speed bulk insert operation using PostgreSQL's COPY protocol. This is significantly faster than bulk_insert for large datasets.
Arguments
objct::SQLObjectHandler: The database handler object (e.g.,M.Model).df_o::DataFrames.DataFrame: The DataFrame containing the data to be inserted.columns: (Optional) Specifies which columns to insert. Can be aString, aPair{String, String}, or aVectorof these.show_query::Bool = false: Iftrue, prints theCOPYcommand (note: data stream is not printed).
The caller's DataFrame is never mutated (and never copied — the pipeline works on a zero-copy wrapper).
The COPY protocol cannot express ON CONFLICT — rows that violate a unique constraint make the whole COPY fail. To skip or merge duplicates, use bulk_insert(...; on_conflict = ...) (#123).
Example
bulk_copy(M.Driver, df)PormG.QueryBuilder.bulk_insert — Method
Inserts multiple rows into the database in bulk from a DataFrame.
Arguments
objct::SQLObjectHandler: The SQL object handler to use for the operation.df_o::DataFrames.DataFrame: The DataFrame containing the data to be inserted.columns: Optional. Specifies the columns to insert and their mappings. Can benothing, aString, aPair{String, String}, or aVectorof these. Ifnothing, all columns from the DataFrame are used.chunk_size::Integer: Optional. The number of rows to insert in each batch (default: 1000).show_query::Symbol: Optional. Return the generated SQL instead of executing it —:sql,:dict,:inspection, or:params(see Query Inspection). Defaults to:execute(run it).on_conflict: Optional (#123). Attaches anON CONFLICTclause so duplicate rows are skipped or merged instead of erroring (PostgreSQL and SQLite share the syntax). Accepts:nothing(default): no clause — a duplicate key raises, as before.:nothing:ON CONFLICT DO NOTHING(untargeted — any unique violation skips the row).(action = :nothing, target = ["field"]):ON CONFLICT (col) DO NOTHING.(action = :update, target = ["field"], set = ["field"]):ON CONFLICT (col) DO UPDATE SET col = EXCLUDED.col, …(upsert).
target/settake logical model field names (resolved throughdb_column).setfields must participate in the INSERT column list.targetis not required to be declared unique in the model — the database is the source of truth for matching constraints. Whenon_conflictis set, the duplicate-key → sequence-resync retry is skipped: a conflict is expected there, not a sequence desync, so any duplicate-key error that still surfaces (a different constraint than the target) propagates.
The caller's DataFrame is never mutated (and never copied — the pipeline works on a zero-copy wrapper), so there is no copy= knob to think about.
Examples
```julia include("models.jl") import models as mdl
Basic usage
query = mdl.User |> object df = DataFrame(name=["Alice", "Bob"], age=[30, 25]) bulk_insert(query, df)
With column mapping and excluding unwanted variables
query = mdl.Boook |> object df = DataFrame(title=["Book A", "Book B"], authorname=["Alice", "Bob"], year=[2020, 2021], ignoreme=["x", "y"])
Map DataFrame column "author_name" to model field "author"
Exclude "ignore_me" by not including it in the columns argument
bulkinsert(query, df, columns=["title", "year", "authorname" => "author"])
Only "title", "year", and "author_name" (as field "author") participate in the INSERT;
the DataFrame's columns are never renamed or removed — the mapping is internal.
```
PormG.QueryBuilder.bulk_update — Method
Performs a bulk update operation on a database table using the provided DataFrame and a query object.
Arguments
objct::SQLObjectHandler: The database handler object.df::DataFrames.DataFrame: The DataFrame containing the data to be used for the update.columns: (Optional) The participating fields and their mappings — the single place a DataFrame column is mapped to a model field. Each entry is aString(DataFrame column == model field) or aPair{String, String}of"df_col" => "model_field". AVectorof these is accepted. Ifnothing, columns are auto-detected from the DataFrame. Fields selected bymatch_onare used for matching only and are not SET.match_on: (Optional) The per-row match keys that identify which row each DataFrame row updates (the SQL merge conditionTb.field = source.col). Bare model field names only — the source column is thecolumns=mapping for that field when you declared one, otherwise a DataFrame column with the field's own name. A value PormG auto-populates for a field left out ofcolumns=(auto_now, or a staticdefaultwhencolumns=is omitted — an explicitcolumns=already suppresses static defaults on an update) is not a declared mapping: your same-named column outranks it, and with no caller source at all the call raisesUnknownFieldErrorrather than matching every row against one per-call constant. If omitted, the model primary key(s) are used and must be present in the DataFrame, under the same precedence. A match key is matched, never written — it stays out of theSETclause, so using anauto_nowfield as a key does not refresh that timestamp.filters: (Optional) Constant predicates AND'd onto theWHEREclause, applied to every row. Each entry is aPair{String, T}of"model_field" => value(e.g."category_id" => 172100,"points__@in" => [18, 25]). Whenmatch_onis provided, everyfiltersentry must be such a constant predicate. A per-row match key infiltersis rejected with a migration error — move it tomatch_on.show_query::Bool: (Optional) Iftrue, prints the generated SQL query. Defaults tofalse.chunk_size::Integer: (Optional) Number of rows to process per chunk. Defaults to1000.
The caller's DataFrame is never mutated (and never copied — the pipeline works on a zero-copy wrapper), so the operation is safe in asynchronous contexts without a copy= knob.
Example
# Update by primary key inferred from the DataFrame
bulk_update(objct, df)
# Set name/dof, matching rows on security_id
bulk_update(objct, df, columns=["name", "dof"], match_on=["security_id"])
# Map differing DataFrame names and add a constant scope guard. ALL df→field
# mappings live in columns=; match_on selects merge keys by model field name
# (a field listed in both is used only for matching — it is not SET).
bulk_update(objct, df,
columns = ["new_score" => "points", # df "new_score" → field "points" (SET)
"record_id" => "id"], # df "record_id" → field "id" (match key)
match_on = ["id"], # match Tb.id = source.id
filters = ["category_id" => 172100]) # constant: only this categoryPormG.QueryBuilder.delete — Method
delete(objct::SQLObjectHandler; show_query=:execute, allow_delete_all=false) -> (total::Int, Dict{String,Integer})Delete every row the query matches, cascading through foreign-key relationships according to each referencing field's on_delete action. The root delete and every dependent statement run inside one transaction, so a failure part-way through leaves the database untouched.
Arguments
objct::SQLObjectHandler: the handler carrying the query and its model.show_query::Symbol = :execute::executeruns the delete.:sql,:dict,:inspectionand:paramsbuild the statements and return them instead of executing them, and:nonebuilds and discards them — seeshow_query. An unrecognized value raisesQueryBuildErrorwhen the statements are rendered, which is after the guards below have run.allow_delete_all::Bool = false: permit a delete whose query carries no filter. Off by default; see the guard table below.table_alias::Union{Nothing, SQLTableAlias} = nothing: accepted for call-signature compatibility with the other terminals; the delete path does not read it.connection::Union{Nothing, PormGPostgres, PormGSQLite} = nothing: execute against this connection pool instead of the one on the model's settings (those two abstract types are the backend markers the concrete pools subtype). Only the pool is overridden; the settings, and therefore the dialect and thechange_dataflag, still come from the model'sconnect_key. Whether the delete joins a surrounding transaction is decided by the task-local transaction context, not by this argument.
Returns
- Under
:execute:Tuple{Integer, Dict{String, Integer}}— the total rows deleted and a per-table breakdown, e.g.(1, Dict("just_a_test_deletion" => 1)). A query matching nothing returns(0, Dict()); a delete that ran but removed no rows warns. - Under
:sql,:dict,:inspectionor:params: the built statement(s) — one per statement the plan emits, returned bare when the plan is a single statement and as aVectorotherwise. A cascade can emit several statements for the same table (oneUPDATEperSET_NULL/SET_DEFAULTfield, plus itsDELETE), so the count tracks statements, not tables. - Under
:none:nothing.
Behavior
Every check below runs before any SQL is generated:
| Guard | Raises |
|---|---|
| A transaction is open on a connection other than the model's | TransactionError |
The connection is configured change_data: false | WritesDisabledError |
The query has limit(), offset() or order_by() set | UnsafeMutationError |
The query has distinct() set | UnsafeMutationError |
The query carries group_by() / aggregate annotations | UnsafeMutationError |
The query has no filter and allow_delete_all is false | UnsafeMutationError |
The three query-shape guards share one rationale: the deletion collector walks the complete filtered set so row counts, cascades and constraint handling stay deterministic. Any shape that truncates or collapses that set is refused rather than quietly applied to part of it — there is no "delete the first N rows" form, so filter by primary key to bound a delete.
Dependent rows are then resolved per referencing field, by that field's on_delete:
CASCADE: the dependents are collected and deleted too, recursing into their own dependents.PROTECT/RESTRICT: raisesProtectedError, naming the referencing model and field. The two behave identically apart from the word in the message. The check is existence-driven — it fires only when referencing rows are actually present, so an empty reverse relation does not block the delete.SET_NULL: issuesUPDATE ... SET <column> = NULLover the dependents instead of deleting them. Declaring it on anull = falsefield is a contradiction the schema cannot satisfy, and raisesModelDefinitionError.SET_DEFAULT: issuesUPDATE ... SET <column> = <the field's default>over the dependents. Declaring it on a field with nodefaultis the mirror-image contradiction, and raisesModelDefinitionErrortoo.DO_NOTHING: PormG emits nothing for the relation and defers to the database's own constraint.
Only CASCADE walks further down the graph; SET_NULL and SET_DEFAULT do not recurse.
Both contradictions are normally caught earlier, at set_models registration; the checks here are the backstop for models that never passed through it.
An unset on_delete — the default for ForeignKey — produces no ORM statement for that relation, leaving the reference entirely to the database's own constraint. It renders ON DELETE NO ACTION in DDL, so a dependent row is not cascaded by PormG unless its field says so explicitly.
The collected statements then execute in dependency order inside a single transaction (BEGIN on PostgreSQL, BEGIN IMMEDIATE TRANSACTION on SQLite), so any failure rolls the whole set back.
Examples
# Delete objects from a model with a specific filter
query = M.Status.objects
query.filter("status" => "Engine")
total, dict = delete(query)
# Build the SQL without executing it
query = M.Just_a_test_deletion.objects
query.filter("test_result__constructorid__name" => "Williams")
sql = delete(query, show_query = :sql)
# Delete related tables (cascading delete)
query = M.Result.objects
query.filter("resultid" => 1)
total, dict = delete(query)
# Delete all objects from a model (use with caution)
query = M.Just_a_test_deletion.objects
total, dict = delete(query; allow_delete_all = true)See also show_query, inspect_query, and Deleting Records.
PormG.QueryBuilder.delete — Method
delete(row::PormGRow; show_query=:execute) -> (total::Int, Dict{String,Integer})Delete this fetched row from its table, cascading through the same DeletionCollector as Model.objects.filter(...).delete() — so on_delete behaviour (CASCADE / SETNULL / PROTECT) is identical whether you delete one fetched row or a filtered set. Returns the `(totaldeleted, per-model counts)` tuple of the underlying queryset delete.
The row is located by its primary key, which must have been projected onto the row (it is, for rows from list()/first()/get()). The in-memory row is not mutated — its data becomes stale after the delete.
PormG.QueryBuilder.earliest — Method
earliest(objct::SQLObjectHandler, fields...; show_query = :execute) -> PormGRowReturn the earliest row ordered by fields (ascending; a "-field" flips that term to descending). Requires at least one field and raises DoesNotExist when no rows match — the extreme-row counterpart of get, matching Django's earliest().
PormG.QueryBuilder.inspect_query — Method
inspect_query(q::SQLObjectHandler) -> DictComprehensive query inspection API that provides full metadata about a query without executing it. Returns a rich dictionary with SQL, parameters, dialect information, and structural metadata.
This is the explicit API for query inspection - use this when you want to examine a query's structure and generated SQL without ambiguity.
Arguments
q::SQLObjectHandler: The query object to inspectoperation::Union{Nothing, Symbol} = nothing: Optional operation override (:select, :insert, :update, :delete). If not provided, the operation is detected automatically based on the query structure.
Returns
Dict: A dictionary containing::sql_text(String): The generated SQL query:parameters(Vector): The parameterized values in bucket order:dialect(Symbol): The database dialect (:postgresqlor:sqlite):model(String): The model/table name:operation(Symbol): The query operation type (:select,:insert,:update,:delete):bucketing(Symbol): The parameter bucketing strategy (:numberedfor PostgreSQL,:positionalfor SQLite):parameter_count(Int): Number of parameters:parameter_buckets(Dict): Breakdown of parameters by bucket (for positional strategies)
Example
q = M.Driver.objects
q.filter("nationality" => "British")
q.order_by("surname")
inspection = q |> inspect_query()
# Dict with:
# :sql_text => "SELECT ... WHERE drivers.nationality = $1 ORDER BY ..."
# :parameters => ["British"]
# :dialect => :postgresql
# :model => "drivers"
# :operation => :select
# :bucketing => :numberedPormG.QueryBuilder.latest — Method
latest(objct::SQLObjectHandler, fields...; show_query = :execute) -> PormGRowReturn the latest row ordered by fields (descending; a "-field" flips that term to ascending). Requires at least one field and raises DoesNotExist when no rows match. Django's latest(); latest("f") == earliest("-f").
PormG.QueryBuilder.list — Method
Return plain Dict{Symbol,Any} rows for framework integrations that need real dictionaries.
PormG.QueryBuilder.list — Method
Return a JSON string without allocating PormGRow wrappers.
PormG.QueryBuilder.list — Method
Return model-aware PormGRow objects. Default format.
PormG.QueryBuilder.object — Method
object(model::PormGModel) -> ObjectHandlerWrap a model in an ObjectHandler — the start of every query. M.Driver.objects is the idiomatic spelling; object(M.Driver) is the same thing as a function call.
This docstring is the fluent-API reference. The methods below are synthesized by getproperty, so they have no bindings of their own — ?query.filter cannot work. ?object (or the API reference) is where to look them up.
Chainable methods
Each mutates the handler and returns it, so calls can be chained or accumulated on a variable.
.filter(pairs...)— addWHEREconditions. Each argument may be aPair, aQ/Qor, an operator expression, anFexpression, or anExists(subquery). Repeated calls accumulate (ANDed), unlike.values/.order_by, which replace their previous call (#199).values(fields...)— choose/annotate the selected columns;"*"selects the main table. Replaces its previous call, last-call-wins (#199).order_by(fields...)— sort; prefix-for descending. Accepts a field path or an alias declared by.values()(#423). Replaces its previous call, matching Django's eachorder_by()clears previous ordering (#199).limit(n)/.offset(n)— pagination, one clause each.page(limit)/.page(limit, offset)— pagination in one call;.page(n)sets the limit only and leaves any offset already on the handler in place. Those are the only two arities — anything else (no argument, three arguments, a non-Integer, a keyword) raisesQueryBuildError, same as.limit(...)/.offset(...)(#272).distinct()— addDISTINCT.db("key")— route the query to another connection pool.on(path, pairs...)— add predicates to theONclause of an existing join path.cjoin("field" => "Model"; filters, join_type)— custom join at query time.cjoin_on(model; alias, on, join_type)— anchor-less join whereonis the entireONclause.with("name" => subquery; join_field, join_type)— define a CTE; call again for a second one.select_for_update(; nowait, skip_locked, no_key)—SELECT … FOR UPDATErow lock.copy()— deep copy, to branch a chain without disturbing the original
Terminal methods
Each executes and returns a result. Every one below except .inspect() takes show_query = :sql / :dict / :params to render instead of executing; .inspect() is already an inspection call and takes operation = / connection = instead.
.list()→Vector{PormGRow};.list(:dict)→Vector{Dict};.list(:json)→ JSONStringquery |> DataFrame— preferred for analytical queries.get(pairs...)— exactly one row, orDoesNotExist/MultipleObjectsReturned.first()/.last()— one row ornothing, using the ordering already on the query (.lastinverts it, falling back to primary-key descending when none is set).earliest(fields...)/.latest(fields...)— replace the ordering withfields(ascending / descending) and take one row; at least one field is required, and an empty queryset raisesDoesNotExistrather than returningnothing.count(column = nothing; distinct = false)/.exists()— checks without fetching rows.aggregate(pairs...)— whole-queryset aggregation with noGROUP BY; returns aNamedTuple.create(pairs...)— insert one row, returned as aPormGRow.update(pairs...)— update every matching row.get_or_create(lookup...; defaults)/.update_or_create(lookup...; defaults)→(row, created).delete()— delete every matching row.inspect()— theinspect_querymetadataDict
Examples
using PormG, DataFrames
using PormG.Functions: Count
# Accumulate on a variable — clearest for multi-step queries
query = M.Result.objects
query.filter("driverid__surname" => "Senna", "positionorder" => 1)
query.values("raceid__year", "raceid__name", "constructorid__name")
df = query |> DataFrame
wins = query.count()
any_ = query.exists()# Inline chain — trailing dots; a leading dot on the next line is a ParseError
podiums = M.Result.objects.
filter("raceid__year" => 2020, "positionorder__@lte" => 3).
values("driverid__surname", "n" => Count("resultid")).
order_by("-n").
limit(10).
list()# Single-row writes — let the IDField allocate the key, then read it off the returned row
row = M.Status.objects.create("status" => "Heat shield fire") # PormGRow
M.Status.objects.filter("statusid" => row.statusid).update("status" => "Heat shield")See also ObjectHandler, show_query, and Reading Data.
PormG.QueryBuilder.pk — Method
pk(row::PormGRow)
pk(row::PormGRow, default)Primary-key value of row, read through its model's declared pk column — so it works for any pk name, not only id. The 1-arg form throws if the model has no single-column primary key, or the pk column is absent from the row. The 2-arg form returns default in those cases instead of throwing (for best-effort callers). A composite (multi-column) primary key has no scalar pk; read the individual key columns instead.
PormG.QueryBuilder.resync_sequences — Method
resync_sequences(objct::SQLObjectHandler) -> Vector{String}
resync_sequences(model::PormGModel) -> Vector{String}
resync_sequences(models::AbstractVector{<:PormGModel}) -> NothingExplicitly repair model's primary-key sequence(s) to MAX(pk) + 1, without performing an insert (#358).
bulk_insert/bulk_copy still resynchronize automatically after a bulk write with explicit primary keys — that is where sequence drift is actually produced in volume. The row-level writers (create/insert, update_or_create, get_or_create) do not auto-resync: call this explicitly after one of them writes an explicit primary key, or after any out-of-band load that can leave a sequence behind — a pg_restore, a manual COPY, or data seeded outside PormG entirely.
Arguments
objct/model: the model (or itsSQLObjectHandler, e.g.M.Model.objects) whose declared primary-key field(s) should be resynchronized.models: a collection, resynced one at a time.
Returns
The names of the pk fields the repair was attempted for (empty if the model has none) — nothing for the plural form. Per-field failure is not raised as an exception here; it follows _update_sequence's own reporting (@warn outside a transaction, propagates inside one — see Sequence synchronisation in schema_conventions.md).
Example
# A migration replays historical drivers with their original ids.
for row in eachrow(legacy_drivers)
M.Driver.objects.create("driverid" => row.id, "forename" => row.forename)
end
resync_sequences(M.Driver) # once, after the batch — not per row
# A later ordinary create() is safe again:
M.Driver.objects.create("forename" => "Auto")PormG.QueryBuilder.save — Method
save(row::PormGRow; show_query=:execute) -> PormGRow | VectorPersist dirty fields assigned on a PormGRow.
Direct fields update the row's own table. Projected fields like driverid__forename update the related table identified by the driverid foreign key value already present on the row.
PormG.QueryBuilder.show_query — Function
show_query(q::SQLObjectHandler, mode::Symbol = :sql)Render a SELECT query without executing it. The default :sql mode returns just the SQL string, which makes it the quickest way to see what a chain builds.
mode | Returns |
|---|---|
:sql | String — the generated SQL |
:params | Vector — the parameterized values, in bucket order |
:dict / :inspection | Dict — the full metadata shape of inspect_query |
:none | nothing — builds and discards, for benchmarking the builder |
query = M.Driver.objects.filter("nationality" => "British").values("forename", "surname")
println(show_query(query)) # SELECT "Tb"."forename" … WHERE "Tb"."nationality" = $1
params = show_query(query, :params) # ["British"]Inspection builds on a deepcopy, so it never mutates the query you pass (#43) — the same chain can be inspected and then executed.
For INSERT/UPDATE/DELETE, pass show_query= to the terminal method itself (query.delete(show_query = :sql)); this function always renders a SELECT. Use inspect_query when you want the metadata Dict with an explicit operation override.
PormG.Models.Index — Type
Index(; fields, name = nothing)Index a combination of columns — Django's Meta.indexes. Pass it to Model through indexes =.
An Index is a read-performance declaration, not a rule: it constrains nothing. For a composite uniqueness guarantee use UniqueConstraint, which is a CREATE UNIQUE INDEX and rejects duplicate rows.
fields names two or more fields on this model. The order is significant: an index over ("raceid", "lap") serves a lookup by raceid, or by raceid and lap together, but not one by lap alone. Foreign keys are referenced by their field name and resolved to the physical column (honoring db_column), and the declared case is preserved, so name each field exactly as it was declared.
A single-column Index is rejected. It is not a missing feature — it is unrepresentable in both directions: a one-column CREATE INDEX is byte-identical whether db_index = true or an Index emitted it, so introspection reads it back as db_index (there is no marker to distinguish them, unlike UniqueConstraint, which SQLite tags origin = 'u' vs 'c'). A model declaring a one-field Index would therefore compare unequal to its own live table forever, and makemigrations would propose dropping the index on every run. Declare db_index = true on the field instead.
name is the index name. Omitted, the migration planner derives <table>_<cols>_idx, the plain sibling of the composite-unique convention. Pass an explicit one when the derived name would exceed PostgreSQL's 63-byte identifier limit, which Postgres truncates with only a NOTICE — truncation can collide two indexes into one.
Invalid declarations raise ModelDefinitionError as early as they can be detected: fewer than two fields, a repeated field, or a blank name fails here in the constructor; a field that does not exist on the model, a ManyToManyField (it owns no column), or two indexes sharing a name fail when the model is built. An index whose name collides with a UniqueConstraint's on the same table fails at migration planning, where both names are known.
Each index becomes a CREATE INDEX — identical on PostgreSQL and SQLite — emitted when its table is first created. Adding or removing one on a table that already exists is not yet detected by makemigrations, the same limitation UniqueConstraint carries. Introspection does read composite indexes back, so inspectdb on an existing database reproduces them. Declare an index with the model, or add it by hand on an existing table.
Examples
Lap_times = Models.Model("lap_times",
raceid = Models.ForeignKey(Race, pk_field = "raceid", on_delete = "CASCADE"),
driverid = Models.ForeignKey(Driver, pk_field = "driverid", on_delete = "RESTRICT"),
lap = Models.IntegerField(),
position = Models.IntegerField(),
indexes = [
Models.Index(fields = ("raceid", "lap"), name = "lap_times_race_lap_idx"),
],
)which migrates to:
CREATE INDEX IF NOT EXISTS "lap_times_race_lap_idx"
ON "lap_times" ("raceid", "lap");See also Model, UniqueConstraint.
PormG.Models.UniqueConstraint — Type
UniqueConstraint(; fields, name = nothing)Require a combination of columns to be unique together — Django's Meta.unique_together, spelled as a named constraint object. Pass it to Model through constraints =; a single-column rule is the field option unique = true instead.
fields names fields on this model — one name, or an iterable of them. Foreign keys are referenced by their field name and resolved to the physical column (honoring db_column), and the declared case is preserved, so name each field exactly as it was declared.
name is the index name. Omitted, the migration planner derives <table>_<cols>_uniq, matching the automatic many-to-many index convention. Pass an explicit one when the derived name would exceed PostgreSQL's 63-byte identifier limit, which Postgres silently truncates — truncation can collide two constraints into one index.
Invalid declarations raise ModelDefinitionError as early as they can be detected: no fields, a repeated field, or a blank name fails here in the constructor; a field that does not exist on the model, a ManyToManyField (it owns no column), or two constraints sharing a name fail when the model is built.
Each constraint becomes a CREATE UNIQUE INDEX — identical on PostgreSQL and SQLite — emitted when its table is first created. Adding or removing one on a table that already exists is not yet detected by makemigrations; it needs composite-index introspection PormG does not have (tracked as a follow-up). Declare composite uniqueness with the model, or add the index by hand.
Examples
Constructor_engine = Models.Model("constructor_engines",
id = Models.IDField(),
constructorid = Models.ForeignKey(Constructor, pk_field = "constructorid", on_delete = "CASCADE"),
year = Models.IntegerField(),
engine_manufacturer = Models.CharField(max_length = 50),
constraints = [
Models.UniqueConstraint(fields = ("constructorid", "year"), name = "uniq_constructor_year"),
],
)which migrates to:
CREATE UNIQUE INDEX IF NOT EXISTS "uniq_constructor_year"
ON "constructor_engines" ("constructorid", "year");See also Model.
PormG.Models.AutoField — Method
AutoField(; kwargs...)Retired (#408). Use IDField. Calling this raises FieldValidationError.
AutoField was documented as a 32-bit auto-incrementing integer primary key — "INTEGER with SERIAL auto-increment". It never was one. Dialect._get_column_type had no sAutoField branch on either backend, so the field fell through to the else and emitted a TEXT column, on PostgreSQL and SQLite alike, with no sequence, identity, or AUTOINCREMENT behind it. A model keyed on it produced a text primary key that nothing could allocate, and makemigrations could never converge, because what the field declared and what introspection read back could not agree.
It is retired rather than repaired because repairing it buys almost nothing and costs a whole class of defect (#409). IDField is the only integer key type PormG's introspection reads back, so any other one is condemned to a perpetual ALTER on every makemigrations. On SQLite the distinction is not even physical: INTEGER PRIMARY KEY is a 64-bit rowid alias, so an AutoField column and an IDField column are byte-identical. The saving was four bytes per row, on one backend, for a type that had never worked — and the Django importer had already been routed away from it for exactly these reasons (#399, DJANGO_AUTO_KEY_TYPES).
This stub exists so a consuming app fails at the declaration with an actionable message instead of an UndefVarError from a generated models file. It is a pre-publish migration aid and is removed before the first General-registry release; see UPGRADING.md.
Migration
# before
Part_category = Models.Model(
id = Models.AutoField(),
name = Models.CharField(max_length = 100)
)
# after
Part_category = Models.Model(
id = Models.IDField(),
name = Models.CharField(max_length = 100)
)IDField is BIGINT rather than INTEGER, but an existing PostgreSQL table whose key really is integer keeps that column: PormG compares the field's declared type slot, not the rendered width. Two caveats, both covered in UPGRADING.md — a key that is not an IDENTITY column still attracts an ADD GENERATED BY DEFAULT AS IDENTITY (a pre-existing :generated mismatch), and a column a real AutoField created is text. PostgreSQL refuses that as an identity column, so the migration errors; 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.
PormG.Models.BigIntegerField — Method
BigIntegerField(; kwargs...)A field for storing 64-bit signed integers, equivalent to PostgreSQL's BIGINT columns.
The BigIntegerField stores large whole numbers within the 64-bit signed integer range (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807). It's ideal for large identifiers, timestamps, population counts, and other numeric data requiring extended range beyond regular integers.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{Int64, Nothing} = nothing: Default value for the fieldeditable::Bool = false: Whether the field should be editable in forms
Database Mapping
- PostgreSQL Type: BIGINT
- Storage: 8 bytes per value
- Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
- Index: Optional, recommended for frequently queried fields
Examples
Large identifier field:
Analytics = Models.Model(
id = IDField(),
user_id = BigIntegerField(db_index=true),
session_id = BigIntegerField(),
timestamp_ms = BigIntegerField() # Unix timestamp in milliseconds
)Population and statistics:
Country = Models.Model(
id = IDField(),
name = CharField(max_length=100),
population = BigIntegerField(null=true),
gdp_usd = BigIntegerField(null=true), # GDP in USD cents
area_sq_meters = BigIntegerField()
)Large external identifiers:
SocialMedia = Models.Model(
id = IDField(),
user = ForeignKey("User"),
twitter_id = BigIntegerField(unique=true, null=true),
facebook_id = BigIntegerField(unique=true, null=true),
follower_count = BigIntegerField(default=0)
)Common Use Cases
- Large Identifiers: External API IDs, social media IDs
- Timestamps: Unix timestamps in milliseconds or microseconds
- Population Data: Country populations, large counts
- Financial Data: Large monetary values in smallest units
- Scientific Data: Large measurements, particle counts
- Analytics: Large user IDs, session identifiers
Migration Considerations
- From IntegerField: Safe upgrade, no data loss
- To IntegerField: Requires validation that all values fit in 32-bit range
- Index Changes: Indexes will be recreated with new size
- Application Code: May need updates if expecting different ranges
PormG.Models.BinaryField — Method
BinaryField(; max_length = nothing, kwargs...)A column for raw binary payloads — images, compressed blobs, encrypted content.
Database Type: BYTEA on PostgreSQL, BLOB on SQLite.
Values are raw bytes in and 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 — the form that keeps a column which used to be TEXT writable without an app edit. To store the decoded bytes of an encoded string, decode it yourself: hex2bytes(s), base64decode(s).
Keyword Arguments
max_length::Union{Int, Nothing} = nothing: maximum payload size in bytes (not characters). Enforced both before the query is built and by aCHECKconstraint in the DDL —octet_lengthon PostgreSQL,lengthon SQLite.nothingmeans unbounded.default::Union{Vector{UInt8}, Nothing} = nothing: rendered into the DDL as a byte literal ('\x…'::bytea/X'…'). Must be aVector{UInt8}; aStringraisesFieldValidationErrorrather than guessing whether you meant its code units or a decoded encoding. Keep it small — it is written verbatim into generated model files.- Plus the common field kwargs:
verbose_name,unique,blank,null,db_index,db_column,editable.
Examples
Technical_document = Models.Model("technical_document",
id = Models.IDField(),
name = Models.CharField(max_length = 200),
file_data = Models.BinaryField(max_length = 5_000_000), # BYTEA / BLOB, ≤ 5 MB
mime_type = Models.CharField(max_length = 100),
)
Technical_document.objects.create(
"name" => "2024 Monza aero package",
"file_data" => read("aero.pdf"), # Vector{UInt8}
"mime_type" => "application/pdf",
)SQLite.jl stores unrecognized Julia values by serializing them into a BLOB, and its reader deserializes any blob carrying that serialization header. A payload PormG wrote is returned verbatim; one written by a different Julia program via sqlserialize may come back as the original object instead of bytes. Inherent to the driver, not to PormG.
PormG.Models.BooleanField — Method
BooleanField(; kwargs...)A field for storing boolean (true/false) values, equivalent to PostgreSQL's BOOLEAN columns.
The BooleanField stores binary true/false values and is ideal for flags, switches, status indicators, and any field that represents a yes/no or on/off state. It maps directly to PostgreSQL's BOOLEAN type and Julia's Bool type.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldunique::Bool = false: Whether values in this field must be unique (rarely used with booleans)blank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{Bool, Nothing} = nothing: Default value for the field (true or false)editable::Bool = false: Whether the field should be editable in forms
Examples
Basic boolean flags:
User = Models.Model(
id = IDField(),
username = CharField(max_length=150),
is_active = BooleanField(default=true),
is_staff = BooleanField(default=false),
email_verified = BooleanField(default=false)
)Boolean Values and Conversion
The field handles various input formats:
- Julia Bool:
true,false - Integers:
1(true),0(false) - Strings:
"true","false","1","0","yes","no" - NULL: When
null=true, acceptsNULL/nothing
PormG.Models.CharField — Method
CharField(; kwargs...)A field for storing short to medium-length strings, equivalent to PostgreSQL's VARCHAR columns.
The CharField is the most commonly used field for storing textual data with a limited length. It maps to a PostgreSQL VARCHAR column and supports validation, indexing, choices, and various constraints. This field is ideal for names, titles, codes, and other string data with known maximum lengths.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldmax_length::Int = 250: Maximum number of characters allowed (1 or greater)unique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddb_column::Union{String, Nothing} = nothing: Map this field to a differently-named physical column (Djangodb_column). Authoritative across DDL, queries, and migrations (#50); defaults to the field namedefault::Union{String, Nothing} = nothing: Default value for the fieldchoices::Union{NTuple{N, Tuple{AbstractString, AbstractString}}, Nothing} = nothing: Restricted set of valid valueseditable::Bool = true: Whether the field should be editable in forms
Length Constraints
- Minimum: 1 character
- Maximum: bounded by the backend, not by PormG — PostgreSQL's
varcharaccepts up to 10,485,760 characters and SQLite ignores the declared length entirely - Validation: Automatically enforced at the field level
- Storage: Efficient variable-length storage in PostgreSQL
Examples
Basic string field:
User = Models.Model(
id = IDField(),
username = CharField(max_length=150, unique=true),
first_name = CharField(max_length=50),
last_name = CharField(max_length=50)
)String field with choices (enumeration):
Order = Models.Model(
id = IDField()
status = CharField(
max_length=20,
choices=(
("1", "Pending"),
("2", "Processing"),
("3", "Shipped"),
("4", "Delivered"),
("5", "Cancelled")
),
default="1"
)
customer_name = CharField(max_length=200)
)Field with a human-readable label (the column name follows the field name, "sku"):
Product = Models.Model(
id = IDField(),
name = CharField(max_length=200),
sku = CharField(
max_length=50,
unique=true,
verbose_name="Stock Keeping Unit"
)
category = CharField(max_length=100, null=true, blank=true)
)Indexed field for performance:
Article = Models.Model(
id = IDField(),
title = CharField(max_length=200, db_index=true),
slug = CharField(max_length=200, unique=true, db_index=true),
content = TextField()
)Choices Feature
The choices parameter allows you to restrict field values to a predefined set:
# Define choices as tuples of (value, display_name)
priority_choices = (
("low", "Low Priority"),
("medium", "Medium Priority"),
("high", "High Priority"),
("urgent", "Urgent")
)
Task = Models.Model(
id = IDField(),
title = CharField(max_length=200),
priority = CharField(max_length=10, choices=priority_choices, default="medium")
)Choice Format Options:
- Tuple of Tuples:
(("value1", "Display 1"), ("value2", "Display 2")) - String Format:
"(value1, Display 1)(value2, Display 2)"
Default Values
- Static Default:
default="some_value" - Must Match Choices: If choices are specified, default must be one of the choice values
- Length Validation: Default value must not exceed
max_length
Database Column Naming
- Conventions: Follow PostgreSQL naming conventions (lowercase, underscores)
CharField vs TextField
| Feature | CharField | TextField |
|---|---|---|
| Length | Bounded (max_length) | Unlimited |
| Database Type | VARCHAR | TEXT |
| Use Case | Short strings | Long content |
| Indexing | Efficient | Less efficient |
| Performance | Fast queries | Slower for large content |
Migration Considerations
- Increasing Length: Safe operation
- Decreasing Length: Requires data validation
- Adding Choices: Application-level change only
- Changing Column Name: rename the field — the column follows the field name;
db_columnis not currently honored by schema generation
Notes
- The field uses VARCHAR type which is efficient for short to medium strings
- Choices are validated at the Julia application level, not in the database
- The
editable=truedefault makes this field suitable for user input forms - Database indexing is optional but recommended for frequently queried fields
- Compatible with PostgreSQL's text search and pattern matching features
See Also
TextFieldfor unlimited length text contentEmailFieldfor email address validation- Database design best practices for string field sizing
PormG.Models.DateField — Method
DateField(; kwargs...)A field for storing date values (without time), equivalent to PostgreSQL's DATE columns.
The DateField stores calendar dates in YYYY-MM-DD format and is ideal for birth dates, event dates, deadlines, and any date information that doesn't require time precision. It maps to PostgreSQL's DATE type and Julia's Date type.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{String, Nothing} = nothing: Default value for the field (YYYY-MM-DD format)editable::Bool = false: Whether the field should be editable in formsauto_now::Bool = false: Whether to automatically set to current date on every saveauto_now_add::Bool = false: Whether to automatically set to current date on creation only
Examples
Basic date fields:
User = Models.Model(
id = IDField(),
username = CharField(max_length=150),
birth_date = DateField(null=true, blank=true),
join_date = DateField(auto_now_add=true),
last_login_date = DateField(null=true)
)Event and scheduling:
Event = Models.Model(
id = IDField(),
title = CharField(max_length=200),
event_date = DateField(db_index=true),
registration_deadline = DateField(),
created_date = DateField(auto_now_add=true)
)Business dates:
Invoice = Models.Model(
id = IDField(),
customer = ForeignKey("Customer"),
issue_date = DateField(auto_now_add=true),
due_date = DateField(),
paid_date = DateField(null=true, blank=true)
)Auto Date Features
autonowadd
Sets the date automatically when the record is first created:
created_date = DateField(auto_now_add=true)
# Automatically set to today's date on creation
# Never changes after initial creationauto_now
Updates the date automatically every time the record is saved:
last_modified_date = DateField(auto_now=true)
# Set to today's date on every save operation
# Useful for tracking last update datesDate Input Formats
The field accepts various input formats:
- Julia Date:
Date(2024, 7, 28) - DateTime:
DateTime(2024, 7, 28, 10, 30)(time ignored) - String ISO:
"2024-07-28" - String formats: Various date strings parseable by Julia
PormG.Models.DateTimeField — Method
DateTimeField(; kwargs...)A field for storing date and time values with timezone information.
Keyword Arguments
verbose_name::Union{String, Nothing}: Human-readable name for the field. Default:nothingunique::Bool: Iftrue, ensures field values are unique across the table. Default:falseblank::Bool: Iftrue, allows empty values in forms/validation. Default:falsenull::Bool: Iftrue, allows NULL values in the database. Default:falsedb_index::Bool: Iftrue, creates a database index for faster queries. Default:falsedefault::Union{DateTime, Nothing, String}: Default value for the field. Can be a DateTime object, ISO string, ornothing. Default:nothingeditable::Bool: Iftrue, field can be edited in forms. Default:falseauto_now::Bool: Iftrue, automatically updates to current datetime on every save. Default:falseauto_now_add::Bool: Iftrue, automatically sets to current datetime when record is created. Default:falsetype::String: The database column type. Can be either"TIMESTAMPTZ"(default) or"TIMESTAMP". Default:"TIMESTAMPTZ"
Important Note: 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.
Examples
# Basic datetime field
created_at = DateTimeField()
# Auto-timestamp fields
created_at = DateTimeField(auto_now_add=true)
updated_at = DateTimeField(auto_now=true)
# Indexed datetime for queries
event_time = DateTimeField(db_index=true, verbose_name="Event Timestamp")
# With default value
scheduled_at = DateTimeField(default=DateTime(2024, 1, 1, 12, 0, 0))
# Optional datetime field
deadline = DateTimeField(null=true, blank=true)```PormG.Models.DecimalField — Method
DecimalField(; kwargs...)A field for storing decimal numbers with fixed precision and scale.
Keyword Arguments
verbose_name::Union{String, Nothing}: Human-readable name for the field. Default:nothingunique::Bool: Iftrue, ensures field values are unique across the table. Default:falseblank::Bool: Iftrue, allows empty values in forms/validation. Default:falsenull::Bool: Iftrue, allows NULL values in the database. Default:falsedb_index::Bool: Iftrue, creates a database index for faster queries. Default:falsedefault::Union{Float64, Nothing}: Default value for the field. Default:nothingeditable::Bool: Iftrue, field can be edited in forms. Default:falsemax_digits::Int: Maximum number of digits allowed (including decimal places). Default:10decimal_places::Int: Number of decimal places to store. Default:2
Examples
# Currency field (2 decimal places)
price = DecimalField(max_digits=10, decimal_places=2)
# High precision scientific values
measurement = DecimalField(max_digits=15, decimal_places=6)
# Percentage with 4 decimal places
rate = DecimalField(max_digits=7, decimal_places=4, default=0.0)
# Financial calculation field
amount = DecimalField(
max_digits=12,
decimal_places=2,
verbose_name="Transaction Amount",
db_index=true
)
# Optional decimal field
discount = DecimalField(
max_digits=5,
decimal_places=2,
null=true,
blank=true
)PormG.Models.DurationField — Method
DurationField(; kwargs...)An elapsed time span — INTERVAL on both PostgreSQL and SQLite.
default is validated at model-definition time and re-raised as FieldValidationError, so a bad default= surfaces where the mistake is rather than on the insert path (where the same coercion raises InvalidValueError).
Examples
Pit_task = Models.Model("pit_task",
id = Models.IDField(),
name = Models.CharField(max_length = 200),
estimated_duration = Models.DurationField(),
actual_duration = Models.DurationField(null = true),
)See also TimeField, DateTimeField.
PormG.Models.EmailField — Method
EmailField(; kwargs...)A field for storing and validating email addresses.
Keyword Arguments
verbose_name::Union{String, Nothing}: Human-readable name for the field. Default:nothingunique::Bool: Iftrue, ensures field values are unique across the table. Default:falseblank::Bool: Iftrue, allows empty values in forms/validation. Default:falsenull::Bool: Iftrue, allows NULL values in the database. Default:falsedb_index::Bool: Iftrue, creates a database index for faster queries. Default:falsedefault::Union{String, Nothing}: Default email address. Default:nothingeditable::Bool: Iftrue, field can be edited in forms. Default:false
Examples
# Basic email field
email = EmailField()
# Unique email for user accounts
user_email = EmailField(unique=true, verbose_name="User Email")
# Optional contact email
contact_email = EmailField(null=true, blank=true)
# Email with default value
notification_email = EmailField(default="admin@example.com")
# Indexed email for fast lookups
primary_email = EmailField(
unique=true,
db_index=true,
)PormG.Models.FileField — Method
FileField(; kwargs...)Django-compatibility alias for storing file upload paths. Behaves identically to ImageField. Accepted kwargs: verbose_name, unique, blank, null, db_index, default, editable, upload_to, max_length.
PormG.Models.FloatField — Method
FloatField(; kwargs...)A field for storing floating-point numbers with double precision.
Keyword Arguments
verbose_name::Union{String, Nothing}: Human-readable name for the field. Default:nothingunique::Bool: Iftrue, ensures field values are unique across the table. Default:falseblank::Bool: Iftrue, allows empty values in forms/validation. Default:falsenull::Bool: Iftrue, allows NULL values in the database. Default:falsedb_index::Bool: Iftrue, creates a database index for faster queries. Default:falsedefault::Union{Float64, String, Int64, Nothing}: Default value for the field. Default:nothingeditable::Bool: Iftrue, field can be edited in forms. Default:false
Examples
# Basic float field
temperature = FloatField()
# Scientific measurement with default
ph_level = FloatField(default=7.0)
# Optional measurement
weight = FloatField(null=true)PormG.Models.ForeignKey — Method
ForeignKey(to::Union{String, PormGModel}; kwargs...)A field that creates a many-to-one relationship to another model, similar to Django's ForeignKey.
The ForeignKey field represents a relationship where many records in the current model can reference a single record in the target model. It creates a foreign key constraint in the database and enables efficient querying of related data.
Required Arguments
to::Union{String, PormGModel}: The target model that this field references. Can be either:- A string with the model name (e.g., "User", "Category")
- A direct reference to a PormGModel instance
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldprimary_key::Bool = false: Whether this field is the primary key (rarely used with ForeignKey)unique::Bool = false: Whether values must be unique (creates a one-to-one relationship if true)blank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = true: Whether to create a database index on this field (recommended for performance)default::Union{Int64, Nothing} = nothing: Default value for the field (ID of the referenced record)editable::Bool = false: Whether the field should be editable in formspk_field::Union{String, Symbol, Nothing} = nothing: Which field in the target model to reference (defaults to primary key)on_delete::Union{Function, String, Nothing} = nothing: Action when the referenced object is deletedon_update::Union{String, Nothing} = nothing: Action when the referenced object's key is updateddeferrable::Bool = false: Whether the constraint check can be deferred until transaction commitinitially_deferred::Bool = false: Whether constraint checking is initially deferredhow::Union{String, Nothing} = nothing: Join type for queries ("INNER JOIN", "LEFT JOIN", etc.)related_name::Union{String, Nothing} = nothing: Name for the reverse relationdb_constraint::Bool = true: Whether to create a database foreign key constraintdb_column::Union{String, Nothing} = nothing: Map the local FK column to a differently-named physical column (#50). The referenced parent column followspk_field(resolved through the parent field's owndb_columnwhen the target is a resolved model). Defaults to the field name
Database Mapping
- PostgreSQL Type: BIGINT with foreign key constraint
- Constraint: Creates
FOREIGN KEYconstraint linking to target table - Index: Automatically indexed for query performance
On Delete Options
The on_delete parameter controls what happens when the referenced object is deleted:
CASCADE: Delete this object when referenced object is deletedRESTRICT: Prevent deletion of referenced object if this object existsSET_NULL: Set this field to NULL (requiresnull=true)SET_DEFAULT: Set this field to its default value (requiresdefaultto be set)PROTECT: Raise an error to prevent deletionDO_NOTHING: Take no action (may cause database integrity errors)
Omitting on_delete is also valid and is the default: PormG then emits no statement for the relation and renders ON DELETE NO ACTION, leaving the reference to the database's own constraint.
The two "requires" above are enforced, not advisory — set_models raises ModelDefinitionError for a SET_NULL field declared null=false or a SET_DEFAULT field with no default (#287).
Examples
Basic foreign key relationship:
Article = Models.Model(
id = IDField()
title = CharField(max_length=200)
author = ForeignKey("User")
category = ForeignKey("Category", on_delete=CASCADE)
)Foreign key allowing NULL values:
Product = Models.Model(
id = IDField()
name = CharField(max_length=100)
category = ForeignKey("Category", null=true, blank=true, on_delete=SET_NULL)
)Multiple foreign keys to the same model — related_name is optional, but recommended:
Message = Models.Model(
id = IDField()
sender = ForeignKey("User", related_name="sent_messages")
recipient = ForeignKey("User", related_name="received_messages")
content = TextField()
)Related Names and Reverse Relations
- If
related_nameis not specified, PormG derives one and logs it at@info - The derivation counts every relation this model declares to that target —
ForeignKey,OneToOneFieldandManyToManyFieldalike: the lowercase model name when it is the only one,<model>_<field>for every member of a group of two or more (#396) - A derived name is never written back onto the field, so
related_namestaysnothingunless you set it - The reverse accessor must not match a field name on the model it lands on, or another accessor already registered there; either raises
ModelDefinitionErroratset_models - The related name allows querying from the target model back to this model
- If you can't remember the related name, you can type
your_query.objects.related_objectsoryour_model.related_objectsto see all related names
Database Constraints
- When
db_constraint=true(default), creates actual foreign key constraints in PostgreSQL - When
db_constraint=false, no database constraint is created (useful for legacy databases) - Database constraints ensure referential integrity but may impact performance
Validation
- The
toparameter must be a valid model name or PormGModel instance - All boolean parameters are validated for type safety
- The
on_deleteparameter is validated against allowed values - Invalid parameters trigger warnings but don't cause errors
Notes
- The field stores the primary key value of the referenced object
- Uses BIGINT type to match IDField primary keys
- Supports deferred constraint checking for complex transactions
- Compatible with PostgreSQL's foreign key features
See Also
- Django's ForeignKey documentation for conceptual understanding
PormG.Models.IDField — Method
IDField(; kwargs...)A field type for auto-incrementing integer primary keys, equivalent to PostgreSQL's BIGSERIAL or GENERATED AS IDENTITY columns.
The IDField is typically used as the primary key for models and automatically generates unique integer values for each record. It maps to a PostgreSQL BIGINT column with auto-increment capabilities.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldprimary_key::Bool = true: Whether this field is the primary key for the tableauto_increment::Bool = true: Whether the field should auto-increment (generate values automatically)unique::Bool = true: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in forms (not applicable for ID fields)null::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = true: Whether to create a database index on this fielddefault::Union{Int64, Nothing} = nothing: Default value for the field (rarely used with auto-increment)editable::Bool = false: Whether the field should be editable in forms (typically false for ID fields)generated::Bool = true: Whether to use PostgreSQL's GENERATED AS IDENTITY featuregenerated_always::Bool = false: Whether to use GENERATED ALWAYS AS IDENTITY (stricter than regular GENERATED)
Database Mapping
- PostgreSQL Type: BIGINT with GENERATED AS IDENTITY or GENERATED ALWAYS AS IDENTITY
- Auto-increment: Supported through PostgreSQL's identity columns
- Index: Automatically indexed as primary key
Examples
Basic usage (most common):
User = Models.Model(
id::PormGField = IDField()
name::PormGField = CharField(max_length=100)
email::PormGField = EmailField()
)Using GENERATED ALWAYS (stricter identity):
Order = Models.Model(
id::PormGField = IDField(generated_always=true)
customer_id::PormGField = ForeignKey("Customer")
order_date::PormGField = DateTimeField()
)Notes
- The
IDFieldis designed to be the primary key and should typically be the first field in your model - Values are automatically generated by the database, so you don't need to provide them when creating records
- The field uses BIGINT type to support large ranges of ID values
- When
generated_always=true, the database will reject any attempts to manually insert ID values - This field type is PostgreSQL-specific and optimized for PormG's PostgreSQL backend
Validation
- All boolean parameters are validated to ensure type safety
- The
verbose_namemust be a String or nothing - The
defaultvalue, if provided, must be convertible to Int64 - Invalid parameters will trigger warnings but won't cause errors (they'll be ignored)
PormG.Models.ImageField — Method
ImageField(; kwargs...)A field for storing image file references and metadata.
Keyword Arguments
verbose_name::Union{String, Nothing}: Human-readable name for the field. Default:nothingunique::Bool: Iftrue, ensures field values are unique across the table. Default:falseblank::Bool: Iftrue, allows empty values in forms/validation. Default:falsenull::Bool: Iftrue, allows NULL values in the database. Default:falsedb_index::Bool: Iftrue, creates a database index for faster queries. Default:falsedefault::Union{String, Nothing}: Default image path or URL. Default:nothingeditable::Bool: Iftrue, field can be edited in forms. Default:false
Examples
# Basic image field
avatar = ImageField()
# Product image with default
product_image = ImageField(
default="/static/images/default-product.jpg",
verbose_name="Product Image"
)
# Optional profile picture
profile_pic = ImageField(null=true)
# Unique banner image
banner = ImageField(
unique=true,
db_index=true
)PormG.Models.IntegerField — Method
IntegerField(; kwargs...)A field for storing 32-bit signed integers, equivalent to PostgreSQL's INTEGER columns.
The IntegerField stores whole numbers within the 32-bit signed integer range (-2,147,483,648 to 2,147,483,647). It's ideal for counts, quantities, ratings, and other numeric data that doesn't require decimal places or extremely large values.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{Int64, Nothing} = nothing: Default value for the fieldeditable::Bool = false: Whether the field should be editable in forms
Examples
Basic integer field:
Product = Models.Model(
id = IDField(),
name = CharField(max_length=200),
quantity = IntegerField(default=0),
price_cents = IntegerField() # Store price in cents to avoid decimals
)Integer field with constraints:
User = Models.Model(
id = IDField(),
username = CharField(max_length=150, unique=true),
age = IntegerField(null=true, blank=true),
score = IntegerField(default=0, db_index=true)
)Rating system:
Review = Models.Model(
id = IDField(),
product = ForeignKey("Product"),
rating = IntegerField(default=5), # 1-5 star rating
helpful_votes = IntegerField(default=0)
)Validation and Constraints
- Range: Automatically validates within INTEGER bounds
- Type: Accepts integers, numeric strings (converted automatically)
- Default: Must be an integer or convertible to integer
- Null: When
null=true, accepts NULL values
Migration Considerations
- Range Changes: Changing to BigIntegerField is safe
- Adding Constraints: Adding uniqueness or indexes is safe
- Default Values: Can be added or modified safely
- Null Constraints: Removing null constraint requires data validation
PormG.Models.JSONField — Method
JSONField(; kwargs...)A field for storing JSON-encoded data.
Maps to PostgreSQL's native JSONB type (binary JSON with indexing support) and stores as TEXT in SQLite. Values are validated as parseable JSON before being sent to the database.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{String, Nothing} = nothing: Default JSON value as a stringeditable::Bool = true: Whether the field should be editable in forms
Database Mapping
- PostgreSQL Type: JSONB
- SQLite Type: TEXT
Examples
Race = Models.Model(
id = IDField(),
name = CharField(max_length=200),
metadata = JSONField(null=true, blank=true, verbose_name="Extra Data")
)Notes
- Values must be valid JSON strings when passed as strings.
- Dict and Vector values are automatically serialized to JSON strings.
- PostgreSQL JSONB supports GIN indexing for efficient key/value lookups.
PormG.Models.ManyToManyField — Method
ManyToManyField(to::Union{String, PormGModel}; kwargs...)Declare a many-to-many relationship without adding a physical column to the owning model table. When through is omitted, migrations synthesize a join table with two foreign keys and a composite unique index.
Keyword Arguments
through::Union{String, PormGModel, Nothing} = nothing: explicit through model; skips auto table synthesis.related_name::Union{String, Nothing} = nothing: reverse accessor on the target model. Omitted, it is derived like aForeignKey's — the lowercase model name, or<model>_<field>when the model declares two or more relations to that target, many-to-many and foreign key counted together (#396).db_table::Union{String, Nothing} = nothing: auto-through table name override. Ignored whenthroughis given — the join table is then the through model's own table (itsdb_tableif it declares one).source_field::Union{String, Nothing} = nothing: the join key pointing at the source model. With an explicitthrough, it names a field on that model and the physical column is resolved from the field'sdb_column(#377); on the auto-synthesized table it names the column directly, since PormG creates it.target_field::Union{String, Nothing} = nothing: the same, for the target model.
Both pins exist to disambiguate which foreign key is which end — required when the through model has two pointing at the same model, as on a self-relation. A through model whose foreign keys simply map to differently-named columns needs no pin: db_column is resolved on its own.
PormG.Models.Model — Method
Model(; constraints = nothing, db_table = nothing, indexes = nothing, fields...)
Model(name; constraints = nothing, db_table = nothing, indexes = nothing, fields...)Define a model — one database table, described by its fields. Returns a Model_Type: the object the query builder starts from, as in M.Driver.objects.
Every keyword other than constraints/db_table/indexes declares a field: field_name = FieldType(...), where the field types are the constructors in this module (IDField, CharField, ForeignKey, …). Declaring a keyword whose value is not a field raises ModelDefinitionError — see the note below, which is the usual reason that happens.
The table name comes from the positional string when given, and otherwise from the Julia binding the model is assigned to, filled in when set_models (or @import_models) registers the module. Both forms are idiomatic and Race = Model(...) and Race = Model("race", ...) name the same table, because a binding-derived name is lowercased as it is filled in. Reach for the positional form when the binding name is not the table name you want — and db_table (below) when even that isn't enough, because the physical table isn't a valid PormG model name at all.
Model("Driver_Profile", …) raises ModelDefinitionError. A positional name is stored verbatim, and the two groups of consumers disagree about case: makemigrations lowercases it into the DDL, while the query builder quotes it as declared. Left unchecked, that model migrated a table named driver_profile and then addressed "Driver_Profile" in every SELECT/INSERT/UPDATE — a table that does not exist on a backend where a quoted identifier is case-sensitive, as it is on PostgreSQL. Rejecting the name at declaration (#300) turns that silent production failure into an error you get at load time.
Model("_order", …) raises the same error, for the same reason: a model name is a lowercase logical identifier, and an underscore-prefixed name is not a shape PormG generates. (It arrived as #306, when format_model_name stripped a leading underscore for a foreign key's REFERENCES target while create_table wrote the stored name as-is, so the model created _order and referenced order. #317 retired that strip, so the split is gone and the rejection is now convention rather than a bug guard.)
Mapping a model to a fixed table whose name those rules reject — arbitrary case, a leading underscore — is db_table (below). The positional name stays the lowercase logical identifier either way.
Field names keep the case you declare and are case-sensitive in queries, so a legacy driverId column is addressed as driverId. A name containing __ is rejected, since that is the lookup separator, and so is a name starting with _:
Models.Model("lap", _end = Models.DateTimeField())
# ModelDefinitionError: The field name '_end' … starts with '_'. …That leading underscore used to be an escape hatch PormG silently stripped — _end = CharField() declared the column end — for a column whose name Julia will not accept as a keyword argument. Retired in #317: db_column (#50) says the same thing explicitly, and composes with db_table.
end_ = Models.DateTimeField(db_column = "end") # field `end_` → column "end"
id2 = Models.CharField(db_column = "_id") # field `id2` → column "_id"Julia's own var"…" syntax also works for a plain field name — var"end" = CharField() declares the field end directly — but it does not escape a model-option collision (see the warning below), so db_column is the spelling that covers every case.
A ManyToManyField is stored on the model but owns no column of its own, so it is absent from field_names and from the created table.
constraints takes UniqueConstraint objects — one, or a collection — for uniqueness spanning more than one column. db_table (#59) pins an explicit physical table name, preserved verbatim — no case fold, no validation beyond "is it a non-empty String" — overriding the name otherwise derived from the positional argument or the binding. It is authoritative everywhere a table identifier is rendered: DDL, SELECT/INSERT/UPDATE/DELETE, JOIN, foreign-key REFERENCES targets, and migration diffing. Unset (the default), a model behaves exactly as it did before this option existed. This is the table-level sibling of CharField's db_column (#50):
# The logical name stays lowercase; db_table carries the exact legacy spelling.
DriverRaces = Models.Model("driver_races", db_table = "Driver_Races_Legacy",
id = Models.IDField(),
)indexes (#347) takes Index objects — one, or a collection — for a read-performance index spanning more than one column, Django's Meta.indexes. A single-column index stays the field option db_index = true.
constraints, db_table and indexes are the only model-level options.
All three are peeled off before the field keywords, so db_table = CharField() declares the option (and raises, since a field is not a String) rather than a column called db_table. To declare a column with one of those names, pin it with db_column:
table_kind = Models.CharField(db_column = "db_table")var"db_table" = CharField() does not help: it parses to the keyword-argument name :db_table, and the peel keys on that name however it was spelled. db_column is the only spelling that genuinely declares the column. A table named db_table needs nothing special — Model("db_table", …) is fine.
There is no model-level ordering or verbose_name — db_table (above) is the one model-level physical-naming option. Any other keyword is read as a field declaration and raises:
Models.Model("race", ordering = ["-year"], raceid = Models.IDField())
# ModelDefinitionError: All fields must be of type PormGField, exemple: …Instead: order at query time with order_by() — there is no per-model default sort; and set verbose_name per field, where it is accepted, rather than per model.
Examples
Circuit = Models.Model( # table `circuit`, inferred from the binding
circuitid = Models.IDField(),
name = Models.CharField(max_length = 100),
country = Models.CharField(max_length = 50),
)
Race = Models.Model(
raceid = Models.IDField(),
year = Models.IntegerField(),
round = Models.IntegerField(),
circuitid = Models.ForeignKey(Circuit, pk_field = "circuitid", on_delete = "CASCADE"),
date = Models.DateField(),
time = Models.TimeField(null = true),
constraints = [ # no two races share a (year, round)
Models.UniqueConstraint(fields = ("year", "round"), name = "race_year_round_uniq"),
],
)
Lap_times = Models.Model(
raceid = Models.ForeignKey(Race, pk_field = "raceid", on_delete = "CASCADE"),
driverid = Models.ForeignKey(Driver, pk_field = "driverid", on_delete = "RESTRICT"),
lap = Models.IntegerField(),
position = Models.IntegerField(),
indexes = [ # many rows per (race, lap) — an index, not a rule
Models.Index(fields = ("raceid", "lap"), name = "lap_times_race_lap_idx"),
],
)
# Porting a legacy schema PormG doesn't control: the live table is `Constructor_Standings`.
ConstructorStandings = Models.Model("constructor_standings",
db_table = "Constructor_Standings",
id = Models.IDField(),
)See also set_models, UniqueConstraint, Index, ForeignKey.
PormG.Models.OneToOneField — Method
OneToOneField(to::Union{String, PormGModel}; kwargs...)A field that creates a one-to-one relationship to another model, similar to Django's OneToOneField.
The OneToOneField represents a strict one-to-one relationship where each record in the current model corresponds to exactly one record in the target model, and vice versa. It's essentially a ForeignKey with a unique constraint that ensures no two records can reference the same target record.
Required Arguments
to::Union{String, PormGModel}: The target model that this field references. Can be either:- A string with the model name (e.g., "UserProfile", "Settings")
- A direct reference to a PormGModel instance
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldprimary_key::Bool = false: Whether this field is the primary key (rarely used with OneToOneField)unique::Bool = true: Whether values must be unique (always true for one-to-one relationships)blank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = true: Whether to create a database index on this field (recommended for performance)default::Union{Int64, Nothing} = nothing: Default value for the field (ID of the referenced record)editable::Bool = false: Whether the field should be editable in formspk_field::Union{String, Symbol, Nothing} = nothing: Which field in the target model to reference (defaults to primary key)on_delete::Union{Function, String, Nothing} = nothing: Action when the referenced object is deletedon_update::Union{String, Nothing} = nothing: Action when the referenced object's key is updateddeferrable::Bool = false: Whether the constraint check can be deferred until transaction commitinitially_deferred::Bool = false: Whether constraint checking is initially deferredhow::Union{String, Nothing} = nothing: Join type for queries ("INNER JOIN", "LEFT JOIN", etc.)related_name::Union{String, Nothing} = nothing: Name for the reverse relationdb_constraint::Bool = true: Whether to create a database foreign key constraintdb_column::Union{String, Nothing} = nothing: Map the local column to a differently-named physical column (#50); defaults to the field name
Database Mapping
- PostgreSQL Type: BIGINT with unique foreign key constraint
- Constraint: Creates
FOREIGN KEYconstraint withUNIQUEconstraint - Index: Automatically indexed for query performance and uniqueness enforcement
One-to-One Relationship Characteristics
- Uniqueness: Each target record can only be referenced by one record in the current model
- Bidirectional: The relationship can be traversed in both directions
- Inheritance: Often used to extend models without modifying the original table
- Profile Pattern: Commonly used for user profiles, settings, or detailed information tables
On Delete Options
The on_delete parameter controls what happens when the referenced object is deleted:
CASCADE: Delete this object when referenced object is deletedRESTRICT: Prevent deletion of referenced object if this object existsSET_NULL: Set this field to NULL (requiresnull=true)SET_DEFAULT: Set this field to its default value (requiresdefaultto be set)PROTECT: Raise an error to prevent deletionDO_NOTHING: Take no action (may cause database integrity errors)
Omitting on_delete is also valid and is the default: PormG then emits no statement for the relation and renders ON DELETE NO ACTION, leaving the reference to the database's own constraint.
The two "requires" above are enforced, not advisory — set_models raises ModelDefinitionError for a SET_NULL field declared null=false or a SET_DEFAULT field with no default (#287).
Examples
Basic one-to-one relationship (User Profile pattern):
User = Models.Model(
id = IDField()
username = CharField(max_length=150, unique=true)
email = EmailField()
)
UserProfile = Models.Model(
id = IDField()
user = OneToOneField("User", on_delete=CASCADE)
bio = TextField(blank=true)
avatar = ImageField(blank=true)
birth_date = DateField(null=true, blank=true)
)One-to-one with null values allowed:
Employee = Models.Model(
id = IDField()
name = CharField(max_length=100)
department = CharField(max_length=50)
)
EmployeeSettings = Models.Model(
id = IDField()
employee = OneToOneField("Employee", null=true, blank=true, on_delete=SET_NULL)
email_notifications = BooleanField(default=true)
theme_preference = CharField(max_length=20, default="light")
)Extending a model without modifying it:
Product = Models.Model(
id = IDField()
name = CharField(max_length=200)
price = DecimalField(max_digits=10, decimal_places=2)
)
ProductDetails = Models.Model(
id = IDField()
product = OneToOneField("Product", on_delete=CASCADE, related_name="details")
detailed_description = TextField()
technical_specs = TextField()
warranty_info = TextField()
)Database Constraints vs. Unique ForeignKey
The two spellings produce the same physical column:
# The same column: the referenced key's type, UNIQUE, plus the foreign-key constraint.
user = OneToOneField("User")
user = ForeignKey("User", unique=true)OneToOneField is nonetheless the spelling to declare, and not only for readability: it is what introspection reports for such a column, on both PostgreSQL and SQLite (#417). Declaring ForeignKey(..., unique=true) leaves the models file naming a different struct than the live schema reads back, and while the two compare equal attribute-for-attribute, the migration planner falls back to a struct-type comparison as soon as any other column in that table changes — which proposes an ALTER that re-renders the column unchanged (a full table rebuild on SQLite).
Validation
- The
toparameter must be a valid model name or PormGModel instance - All boolean parameters are validated for type safety
- The
on_deleteparameter is validated against allowed values - Uniqueness is automatically enforced at the database level
- Invalid parameters trigger warnings but don't cause errors
See Also
- Django's OneToOneField documentation for conceptual understanding
- Database normalization principles for when to use one-to-one relationships
PormG.Models.PasswordField — Method
PasswordField(; kwargs...)A VARCHAR(128) field for storing a Django-format password hash.
PasswordField is a storage type only: PormG does not hash or verify passwords. Produce the hash in your application and assign the resulting string to this field. The stored format matches Django's authentication system, so tables written this way stay compatible with Django's own auth code.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valueseditable::Bool = true: Whether the field should be editable in formsmax_length::Int = 128: Maximum length for stored hash (Django default)auto_hash::Bool = true: Accepted for Django compatibility; PormG performs no hashing
Database Mapping
- PostgreSQL Type: VARCHAR(128)
- Storage Format:
pbkdf2_sha256$iterations$salt$base64hash - Index: Not indexed by default (passwords shouldn't be queried)
Stored Format
The password is stored in Django-compatible format:
pbkdf2_sha256$720000$salt$base64encodedHashWhere:
pbkdf2_sha256: Algorithm identifier720000: Number of iterationssalt: Random 22-character saltbase64encodedHash: The derived key in base64
Examples
Basic password field:
# Define a User model with password
User = Models.Model(
id = Models.IDField(),
username = Models.CharField(max_length=150, unique=true),
email = Models.EmailField(unique=true),
password = Models.PasswordField()
)Hashing and verification live in your application, not in PormG — generate the Django-format hash there and assign the resulting string to the field.
Migration from Django
If you're migrating from a Django application, password hashes are fully compatible. Users can continue to log in without any password reset.
See Also
CharFieldfor generic string storage- Django's password management documentation
PormG.Models.PositiveIntegerField — Method
PositiveIntegerField(; kwargs...)A field for storing non-negative whole numbers, equivalent to PostgreSQL's INTEGER columns guarded by a CHECK (col >= 0) constraint. Mirrors Django's PositiveIntegerField.
Values are restricted to the range 0..2147483647. On PostgreSQL the column is declared integer; on SQLite it is declared INTEGER UNSIGNED (INTEGER affinity), which keeps the declared type distinct from IntegerField so the migration engine round-trips the field without drift. On PostgreSQL the introspection layer instead detects the column's non-negative CHECK constraint to tell the two fields apart. The non-negative constraint is enforced both at construction (rejecting negative defaults) and at the database level via a CHECK constraint, which the migration engine adds or drops when a column's type transitions into or out of this field.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{Int64, Nothing} = nothing: Default value for the field (must be 0..2147483647)editable::Bool = false: Whether the field should be editable in forms
Database Mapping
- PostgreSQL Type: INTEGER +
CHECK ("col" >= 0) - SQLite Type: INTEGER UNSIGNED (INTEGER affinity) +
CHECK ("col" >= 0) - Range: 0 to 2147483647
Examples
Lap_times = Models.Model(
id = IDField(),
lap = PositiveIntegerField(default=1),
milliseconds = PositiveIntegerField()
)PormG.Models.PositiveSmallIntegerField — Method
PositiveSmallIntegerField(; kwargs...)A field for storing small, non-negative whole numbers, equivalent to PostgreSQL's SMALLINT columns guarded by a CHECK (col >= 0) constraint. Mirrors Django's PositiveSmallIntegerField.
Values are restricted to the range 0..32767. On PostgreSQL the column is declared smallint; on SQLite it is declared SMALLINT (INTEGER affinity), which preserves the declared type so the migration engine round-trips the field without drift. The non-negative constraint is enforced both at construction (rejecting negative defaults) and at the database level via a CHECK constraint. The migration engine keeps that constraint in sync with the model: on PostgreSQL it is added or dropped when a column's type transitions into or out of this field, and on SQLite it is re-derived whenever the table is recreated during an alter.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{Int64, Nothing} = nothing: Default value for the field (must be 0..32767)editable::Bool = false: Whether the field should be editable in forms
Database Mapping
- PostgreSQL Type: SMALLINT +
CHECK ("col" >= 0) - SQLite Type: SMALLINT (INTEGER affinity) +
CHECK ("col" >= 0) - Range: 0 to 32767
Examples
Standing = Models.Model(
id = IDField(),
position = PositiveSmallIntegerField(default=1),
points = PositiveSmallIntegerField(default=0)
)PormG.Models.SlugField — Method
SlugField(; kwargs...)A field for storing URL-friendly slug strings.
Slugs may contain only lowercase letters, numbers, hyphens, and underscores. Maps to VARCHAR(max_length) in the database. Typically used for human-readable URL fragments derived from titles or names.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldmax_length::Int = 50: Maximum number of characters allowedunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = true: Whether to create a database index on this field (true by default for slugs)default::Union{String, Nothing} = nothing: Default slug valueeditable::Bool = true: Whether the field should be editable in forms
Database Mapping
- PostgreSQL Type: VARCHAR(max_length)
- SQLite Type: TEXT(max_length)
Examples
Race = Models.Model(
id = IDField(),
name = CharField(max_length=200),
slug = SlugField(unique=true, verbose_name="URL Slug")
)PormG.Models.TextField — Method
TextField(; kwargs...)A field for storing large amounts of text without length restrictions.
Keyword Arguments
verbose_name::Union{String, Nothing}: Human-readable name for the field. Default:nothingunique::Bool: Iftrue, ensures field values are unique across the table. Default:falseblank::Bool: Iftrue, allows empty values in forms/validation. Default:falsenull::Bool: Iftrue, allows NULL values in the database. Default:falsedb_index::Bool: Iftrue, creates a database index for faster queries. Default:falsedefault::Union{String, Nothing}: Default text content. Default:nothingeditable::Bool: Iftrue, field can be edited in forms. Default:false
Examples
# Basic text field for long content
description = TextField()
# Blog post content
content = TextField(blank=true)
# Optional notes field
notes = TextField(null=true, blank=true)
# Indexed text field for search
searchable_content = TextField(
db_index=true
)
# Text field with default content
template = TextField(
default="Enter your text here...",
verbose_name="Template Content"
)PormG.Models.TimeField — Method
TimeField(; kwargs...)Time of day with no date component — SQL TIME.
default accepts a Time or anything Time(x) parses (e.g. "09:30:00"); an invalid value raises FieldValidationError at model-definition time rather than on the first insert.
Examples
Team_store = Models.Model("team_store",
id = Models.IDField(),
name = Models.CharField(max_length = 100),
opening_time = Models.TimeField(),
closing_time = Models.TimeField(null = true),
)See also DateField, DateTimeField, DurationField.
PormG.Models.URLField — Method
URLField(; kwargs...)A field for storing URLs, validated against a basic URL pattern.
Maps to VARCHAR(max_length) in the database. Values are validated to start with http://, https://, or ftp://.
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldmax_length::Int = 200: Maximum number of characters allowedunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{String, Nothing} = nothing: Default URL valueeditable::Bool = true: Whether the field should be editable in forms
Database Mapping
- PostgreSQL Type: VARCHAR(max_length)
- SQLite Type: TEXT(max_length)
Examples
Circuit = Models.Model(
id = IDField(),
name = CharField(max_length=200),
wiki_url = URLField(null=true, blank=true, verbose_name="Wikipedia Link")
)PormG.Models.UUIDField — Method
UUIDField(; kwargs...)A field for storing universally unique identifiers (UUIDs).
Maps to PostgreSQL's native UUID type and stores as TEXT in SQLite. Values are validated against the standard UUID format (8-4-4-4-12 hex digits).
Keyword Arguments
verbose_name::Union{String, Nothing} = nothing: A human-readable name for the fieldprimary_key::Bool = false: Whether this field is the primary key for the tableunique::Bool = false: Whether values in this field must be unique across all recordsblank::Bool = false: Whether the field can be left blank in formsnull::Bool = false: Whether the database column can store NULL valuesdb_index::Bool = false: Whether to create a database index on this fielddefault::Union{String, Nothing} = nothing: Default UUID value as a stringeditable::Bool = true: Whether the field should be editable in formsauto_add::Bool = false: If true, automatically generates a UUID (uuid4()) when creating a new record without a provided value.
Database Mapping
- PostgreSQL Type: UUID
- SQLite Type: TEXT
Examples
using UUIDs
Session = Models.Model(
id = IDField(),
session_token = UUIDField(unique=true, db_index=true),
user_id = ForeignKey("User")
)PormG.Models.set_models — Method
set_models(_module::Module, path::String) -> nothingRegister every model defined in _module against the database configuration folder path, and resolve the relationships between them. This is the manual model-loading path; prefer PormG.@import_models (or PormG.@models_module for inline definitions), which call it for you and also handle precompilation and Revise reloads.
Until a model is registered it is inert: it has no connection and no reverse relations. Building a query still appears to work — M.Driver.objects.filter(...) returns a handler — but rendering or executing it raises InvalidConfigurationError.
Registration does four things:
Names the unnamed. A model declared without a positional table name carries
name == ""; it is filled in here from the Julia binding, lowercased (Race = Model(...)→ tablerace).Binds the connection.
pathis matched against the configured folders to find the connection key. A folder that is not loaded yet is loaded implicitly — which also fixes the environment, so callPormG.Configuration.load(path; env = ...)first when you need a specific one, and expectMissingConfigurationErrorfrom that implicit load ifpathholds noconnection.yml.Resolves relationships. Each
ForeignKey/OneToOneFieldtarget is resolved (a target given as a model-nameStringis replaced by the model object),pk_fielddefaults are applied, and the reverse accessor is installed on the target. An omittedrelated_nameis derived and logged: the lowercase model name when it is the model's only relation to that target,<model>_<field>for every member of a group of two or more — countingForeignKey,OneToOneFieldandManyToManyFieldtogether (#396). A derived accessor is never written back onto the field. In every case the accessor is checked against the target's field names and its already-registered accessors; either clash raisesModelDefinitionError.ManyToManyFields get their join-table metadata built and cached.Rejects contradictions.
on_delete = SET_NULLon anull = falsefield, orSET_DEFAULTwith nodefault, raisesModelDefinitionErrorhere — at declaration, rather than later as a mangledUPDATE. Every such contradiction in the module is collected and reported in that one error, naming each offending model, field and fix, so a legacy schema carrying several of them is diagnosed in a single pass instead of one registration per field (#303). Only these two are aggregated: every other registration error — an unresolvable foreign-key or many-to-many target, a duplicaterelated_name, a model without exactly one primary key, an unusable explicitthroughmodel — raises the same type but still on the first occurrence, and preempts the aggregated report when present.Because the aggregated throw comes after every model is wired, a swallowed failure leaves a fully-wired, queryable graph. The
__init__that@import_modelsand@models_moduleinject, and the Revise reload callback, allcatch— though the first load of either macro still surfaces the error.delete()keeps its own copy of both checks, and that is what still raises for a contradiction the caller never saw.
Calling it again is safe and is the supported way to pick up edits: reverse relations and many-to-many caches are cleared before being rebuilt, so a reload cannot accumulate duplicates.
Examples
module f1_models
import PormG.Models
Circuit = Models.Model(
circuitid = Models.IDField(),
name = Models.CharField(max_length = 100),
)
Race = Models.Model(
raceid = Models.IDField(),
year = Models.IntegerField(),
circuitid = Models.ForeignKey(Circuit, pk_field = "circuitid", on_delete = "CASCADE"),
)
Models.set_models(@__MODULE__, "db") # tables `circuit` / `race`; Circuit gains a `race` accessor
endSee also Model, PormG.@import_models, PormG.@models_module.
PormG.Migrations.MIGRATION_FORMAT_VERSION — Constant
MIGRATION_FORMAT_VERSIONVersion of PormG's frozen migration format contract — the on-disk migration-file layout, the checksum algorithm, and the pormg_migrations tracking-table schema. Every record this engine writes is stamped with this value (the format_version column), and generated migration files carry it as a # pormg-migration-format: N header. It is the single source of truth referenced wherever the format version is written.
1 is the contract documented under Migrations → Format Stability. Bump this only alongside a documented forward-migration path; never repurpose an existing version number.
PormG.Migrations.DestructiveMigrationError — Type
DestructiveMigrationError(msg, statements)Raised when a migration containing destructive operations (DROP TABLE, DROP COLUMN, …) is applied in a non-interactive context (no TTY, or interactive=false) without destructive=true. Failing loudly here means CI, Pkg.test, and deploy scripts break with an actionable message instead of hanging on readline() or silently skipping the migration.
Reparented from Exception to MigrationError <: PormGError (#239). Catching DestructiveMigrationError specifically is unaffected; it is merely ALSO catchable as MigrationError / PormGError. It keeps its own showerror (a more specific method wins).
PormG.Migrations.DryRunResult — Type
DryRunResultResult of a dry-run migration analysis.
Contains only the substantive fields needed to evaluate the migration plan. Use is_destructive(r) and total_statements(r) for derived properties.
PormG.Migrations.MigrationStatus — Type
MigrationStatusStructured result from status(). Contains applied migrations, pending files, failed migrations, and drift signals.
PormG.Migrations.compute_checksum — Method
compute_checksum(sql_content::String) -> StringCompute a SHA-256 hex digest of the SQL content for integrity verification.
PormG.Migrations.convertSQLToModel — Method
convertSQLToModel(sql::String)
Converts a SQL CREATE TABLE statement into a model definition in PormGModel.
Arguments
sql::String: The SQL CREATE TABLE statement.
Returns
PormGModel: The model definition.
Example
PormG.Migrations.convert_schema_to_models — Method
convertschematomodels(db::PormGPostgres; ignoretable::Vector{String} = postgresignoretable)
Convert the database schema to models.
Arguments
db::PormGPostgres: The database connection.ignore_table::Vector{String}: A vector of table names to ignore. Defaults topostgres_ignore_table.
Returns
models_array::Vector{Any}: A vector containing the converted models.
Description
This function retrieves the database schema and converts it to models. It collects all create instructions and skips tables specified in the ignore_table vector. The function prints the type of each schema and returns the schema for debugging purposes. It stops processing after the fifth schema.
PormG.Migrations.detect_destructive_actions — Method
detect_destructive_actions(statements::Vector{String}) -> Vector{String}Return the subset of SQL statements that contain destructive operations.
PormG.Migrations.discard_pending_migration — Method
discard_pending_migration(settings; backup=true) -> NamedTuple | Nothing
discard_pending_migration(db::String; config=config, backup=true) -> NamedTuple | NothingDiscard the un-applied pending migration draft (migrations/pending_migrations.jl) for this connection — e.g. a makemigrations plan you generated and then regretted.
A pending migration is only a file with no database state behind it, so this is filesystem-only: it never touches the pormg_migrations history table or the schema (unlike remove_migration_record / migrate_to, which mutate applied state). That makes discarding a draft the one inherently safe, reversible migration op.
When backup=true (default) the file is renamed to pending_migrations.jl.discarded (overwriting any previous discard) so the draft can be recovered; otherwise it is deleted. makemigrations overwrites the pending file anyway, so a later regenerate is unaffected.
Returns (discarded=true, path, backup, tables, statements) describing what was thrown away, or nothing when there is no pending migration. Pairs with status, which reports whether a pending file exists.
PormG.Migrations.django_to_string — Method
django_to_string(path::String) -> Union{String, Nothing}Read a Django models.py and return its text, ready for import_models_from_django.
A missing file logs a warning and returns nothing rather than throwing.
Until #340 this also rewrote every apostrophe to a double quote so the line-based parser saw one string delimiter. That was a blunt global replace: it broke any apostrophe inside a value (a help_text reading Don't became Don + stray quote + t) and rewrote single-quoted Python docstring delimiters as well. The scanner below tracks both delimiters, and both triple-quoted forms, natively — so the text is now handed over verbatim.
django_to_string("/home/user/models.py") |> import_models_from_djangoPormG.Migrations.dry_run — Method
dry_run(connection, settings) -> DryRunResultAnalyze pending migrations without applying them. Validates ordering, checksums, destructive actions, and SQL generation. Does NOT modify the database or move files.
PormG.Migrations.get_migration_plan — Method
get_migration_plan(models, current_schema, conn, settings; interactive = true)Diff the model definitions against the live database schema and return the DDL that would reconcile them, as an OrderedDict{Symbol, OrderedDict{String, String}} — model name ⇒ ordered (human description ⇒ SQL statement). It only computes the plan; nothing is written or executed. makemigrations is the entry point that drives it.
models is the old schema, reverse-engineered from the database. current_schema is the new state defined in your models.jl. The names predate the current terminology and are kept to avoid churning the planner's unit tests.
An empty models means an empty database, so every model becomes a CREATE TABLE.
With interactive = true (the default) a model with no matching table prompts whether it is new or a rename of a table that disappeared, so a rename keeps its data. interactive = false answers "new table" and "not a rename" for everything — a non-interactive run therefore never renames, it drops and creates. Choosing a nonexistent option at the prompt raises InvalidMigrationError.
PormG.Migrations.import_models_from_django — Method
importmodelsfromdjango(apps::AbstractVector{<:Pair}; db::String = DBPATH, forcereplace::Bool = false, file::String = "automaticmodels.jl", outputpath::Union{Nothing, String} = nothing, authusermodel::Union{Nothing, String} = nothing, strictrelations::Bool = false, strictfields::Bool = false, bindingoverrides::AbstractDict = Dict{String, String}(), autofieldsignore::Vector{String} = ["Manager"], parametersignore::Vector{String} = ["help_text"])
Import a multi-app Django project — "<app_label>" => "<models.py path or source>" pairs — into one generated module.
import_models_from_django(
["core" => "server/core/models.py",
"access" => "server/access/models.py",
"imports" => "server/imports/models.py"];
db = "sgrh", file = "models.jl", force_replace = true)Why one module and not one per app
A Django project with N apps is ONE database, and PormG is structurally one models file per connection: makemigrations/migrate resolve a single joinpath(db, settings.model_file) and load it into a single module. One module per app would not merely be unsupported — it would be invisible to the migration engine. Emitting every app into one module also makes every cross-app foreign key a same-module binding, which is the only thing _resolve_target_model can resolve.
Each model's physical table is <app_label>_<lowercased class name> (Django's own derivation), pinned as db_table, so one file carries every app's tables. A Meta.db_table still overrides it.
Class-name collisions
When two apps declare the same class name, both are renamed to <app>_<class> — binding Core_pessoa and Access_pessoa, never Pessoa and Pessoa2. Renaming only the second would make the output depend on the order the apps were listed, and set_models keys reverse accessors on the logical name, so one model would answer to pessoa and the other to pessoa2. The rename is lossless because db_table carries the real table either way. Use binding_overrides to choose a different spelling.
Arguments
Every keyword of the single-app method applies, except django_prefix: the app label now comes from each pair. A django_prefix on the resolved config is rejected, not ignored — get_model_name would strip that one prefix from every logical name, so core_pessoa would become pessoa while access_pessoa survived intact, and the reverse lookup would then want Pessoa while the binding is Core_pessoa. Cheap to detect here; near-impossible to diagnose at query time.
Relation targets
"self", "<app_label>.<ClassName>", a bare "<ClassName>" (same app first, then a globally unique one), and settings.AUTH_USER_MODEL all resolve. A target outside the imported app set keeps its column and loses the relation, with a # PormG: marker — see strict_relations.
See also import_models_from_django(::String), django_to_string.
PormG.Migrations.import_models_from_django — Method
importmodelsfromdjango(modelpystring::String; db::String = DBPATH, forcereplace::Bool = false, file::String = "automaticmodels.jl", outputpath::Union{Nothing, String} = nothing, djangoprefix::Union{Nothing, String, Missing} = missing, authusermodel::Union{Nothing, String} = nothing, strictrelations::Bool = false, strictfields::Bool = false, bindingoverrides::AbstractDict = Dict{String, String}(), autofieldsignore::Vector{String} = ["Manager"], parametersignore::Vector{String} = ["helptext"])
Imports Django models from a given model.py file content string and generates corresponding Julia models.
For a project whose models are split across several Django apps, pass "<app_label>" => "<path>" pairs instead — see the Vector{Pair} method.
Arguments
model_py_string::String: The content of themodel.pyfile as a string; user djangotostring(path) to read the file; or insert the file path.db::String: The configuration key used to resolve settings (usually the db folder path). The generated file is written to that configuration'sdb_def_folderunlessoutput_pathoverrides it, and the table-name prefix comes from that configuration'sdjango_prefixunlessdjango_prefixoverrides it. Defaults toDB_PATH.force_replace::Bool: Iftrue, forces replacement of the existing models file. Defaults tofalse.file::String: The name of the file to save the generated models. Defaults to"automatic_models.jl".output_path::Union{Nothing, String}: Directory to write the generated file into, overriding the resolved config'sdb_def_folder. Use this to stage a foreign Django app's models next to their copiedmodel.py(e.g."db_gal") while still resolvingdbfor its Settings. Defaults tonothing(usedb_def_folder).django_prefix::Union{Nothing, String, Missing}: The Django app label whose tables are being imported — in practice, the prefix Django puts on them.missinginherits the resolved config'sdjango_prefix;nothingemits unprefixed table names; aString(e.g."estoque") forces<prefix>_<table>. Use this when the imported app uses a different Djangoapp_labelthan thedbconfig's. Defaults tomissing(inherit).Since #345 the prefix is written into
db_table, not into the positional model name:class Dim_ibgeunderdjango_prefix = "estoque"emitsDim_ibge = Models.Model("dim_ibge", db_table = "estoque_dim_ibge", …). AMeta.db_tablein the Django source still overrides it, exactly as in Django. An empty string is treated as no prefix.Each auto-derived
ManyToManyFieldalso gets its join table pinned to Django's spelling, which is<the owning model's table>_<field>— soestoque_dim_ibge_ufsnormally, but<Meta.db_table>_<field>when the class declares one. PormG's own derivation (<logical model>_<field>) reproduces neither. A field carrying its owndb_table, or athrough=, is left alone.auth_user_model::Union{Nothing, String}: which modelsettings.AUTH_USER_MODELrefers to, spelled as Django spells it ("access.User", or a bare"User"when unambiguous). Defaults tonothing, which auto-detects the single class inheritingAbstractUser. If a relation namessettings.AUTH_USER_MODELand there is not exactly one candidate, the import raisesInvalidMigrationErrornaming them — deliberately hard, since one omitted keyword would otherwise turn every user relation in the project into a plain integer column.strict_relations::Bool: whenfalse(the default), a relation whose target is not in this import keeps its column and loses only the relation metadata, with a# PormG:marker saying so.trueraisesInvalidMigrationErrorinstead. The lenient default is what makes the importer usable on a project that touchesdjango.contrib.strict_fields::Bool: whenfalse(the default), a field whose Django type PormG does not implement (GenericIPAddressField,SmallIntegerField, …) is skipped — the column is not imported, and a@warnplus a# PormG:marker name the field, its class and itsmodels.pyline.trueraisesInvalidMigrationErrorinstead. The lenient default exists because the alternative is not "one bad column": before it, a single unimplemented type aborted the import of every model in every app of the call. Note the skipped column still exists in the database, somakemigrationsreads it as drift and proposes dropping it until you declare it by hand — which is the casetrueis for.binding_overrides::AbstractDict:"<app_label>.<ClassName>" => "<JuliaBinding>"(or a bare class name when unambiguous), to spell a generated binding differently from the derived one. The value must be a legal, capitalized Julia identifier that no other model claims; every violation is an error rather than a silent fallback.autofields_ignore::Vector{String}: Fields to ignore automatically. Defaults to["Manager"].parameters_ignore::Vector{String}: Parameters to ignore during field processing. Defaults to["help_text"].
Description
This function checks if the specified models file already exists and creates it if necessary. It parses the provided model.py content string to extract Django model classes and their fields. For each class, it processes the fields, adds a primary key if none exists, and generates the corresponding Julia model code. The generated models are then saved to the specified file.
Relation targets are resolved to the Julia binding of the model they name (#346): "self", "<app_label>.<ClassName>" naming this file's own app, and settings.AUTH_USER_MODEL all work — all three used to reach the generated file verbatim and throw at set_models.
output_path and django_prefix never mutate the shared db config: when either is set, a throwaway render-only Settings (no database connection) drives the output directory and prefix.
Example
importmodelsfromdjango(djangoto_string("/home/user/models.py"))
Stage a foreign Django app (its models.py copied into db_gal/) with its own folder and prefix:
importmodelsfromdjango("dbgal/models.py"; db="db", file="galmodels.jl", outputpath="dbgal", djangoprefix="estoque", force_replace=true)
PormG.Migrations.import_models_from_postgres — Method
import_models_from_postgres(db::String; force_replace::Bool=false, ignore_table::Vector{String}=postgres_ignore_table, file::String="automatic_models.jl")Import models from a PostgreSQL database and generate a Julia file with model definitions.
Arguments
db::String: The database key from the configuration.force_replace::Bool=false: Whether to overwrite the file if it already exists.ignore_table::Vector{String}=postgres_ignore_table: A vector of table name patterns to ignore.file::String="automatic_models.jl": The output filename for the generated models.
Description
This function retrieves the database schema from PostgreSQL, converts each table to a PormG model, and generates a Julia module file containing all the model definitions. The generated file can be directly included in your project to work with the database tables.
Example
using PormG
# Load the database configuration
PormG.Configuration.load("db")
# Import models from the database
PormG.Migrations.import_models_from_postgres("db")
# Or with options
PormG.Migrations.import_models_from_postgres("db", force_replace=true, file="my_models.jl")PormG.Migrations.import_models_from_sqlite — Function
import_models_from_sqlite(db::String="db"; force_replace::Bool=false, ignore_schema::Vector{String}=sqlite_ignore_schema, include_table=nothing, file::String="automatic_models.jl")Import models from a SQLite database and generate a Julia file with model definitions.
Arguments
db::String="db": The database key from the configuration (must resolve to a registered SQLite connection).force_replace::Bool=false: Whether to overwrite the file if it already exists.ignore_schema::Vector{String}=sqlite_ignore_schema: Table name patterns to ignore.include_table::Union{Vector{String},Nothing}=nothing: When set, only these tables are imported.file::String="automatic_models.jl": The output filename for the generated models.
Description
Symmetric with import_models_from_postgres: the database key resolves to its settings via Configuration.get_settings, the output directory comes from that connection's db_def_folder, and the connection object is taken from the same settings — so the key and its settings can never drift apart. A missing/unregistered key raises a clear ArgumentError from get_settings (no silent MODEL_PATH fallback); a key bound to a non-SQLite connection is rejected explicitly.
Example
using PormG
# Load the SQLite configuration
PormG.Configuration.load("db_sl")
# Import models from the database
PormG.Migrations.import_models_from_sqlite("db_sl")PormG.Migrations.init_migrations — Method
init_migrations(connection::Union{PormGPostgres, PormGSQLite})Create the pormg_migrations history table if it does not already exist. This is called automatically by migrate() and status() but can be invoked explicitly for bootstrapping.
PormG.Migrations.is_destructive — Method
Whether the dry-run result contains any destructive statements.
PormG.Migrations.is_destructive — Method
is_destructive(sql::String) -> BoolCheck if a SQL statement contains destructive operations (DROP TABLE, DROP COLUMN, etc.).
PormG.Migrations.makemigrations — Method
makemigrations(db::String; interactive = true)
makemigrations(connection, settings::PormGSettings; path = "db/models.jl", interactive = true)Compare your models.jl against the live database and write the pending migration plan. The first form is the one to call: db is a connection key from your configuration, e.g. makemigrations("db").
It does not touch the schema. The generated DDL lands in <db_def_folder>/migrations/pending_migrations.jl for review; apply it with PormG.Migrations.migrate(db).
Keyword arguments
path: the models file. Defaults to<db>/<settings.model_file>in theStringform.interactive: whentrue, a model with no matching table prompts whether it is a new table or a rename of one that disappeared — a rename preserves the data.falseanswers "new table" for everything and so never renames; use it in CI, not on real data.
Returns nothing. Logs and returns early — writing no plan — when the connection has change_db: false. An up-to-date schema logs that no migrations are pending. A missing models file raises MissingConfigurationError.
See also migrate, get_migration_plan, and the Database Migrations in PormG guide.
PormG.Migrations.mark_applied — Method
mark_applied(connection, settings, version, name; checksum, sql_content)Manually mark a migration version as applied in the history table. Useful for reconciliation after manual intervention or interrupted migrations.
Requires either sql_content (preferred) or an explicit checksum so the recorded digest is verifiable — passing neither is refused rather than fabricated (issue #81). Destructiveness is classified from sql_content when provided instead of being hard-coded, so a manually-reconciled destructive migration is still flagged in history.
PormG.Migrations.mark_failed — Method
mark_failed(connection, settings, version)Update an existing migration record to 'failed' status. Useful after manual investigation of a partially-applied migration.
PormG.Migrations.migrate — Method
migrate(connection::PormGBackend, settings; interactive, destructive, dry_run_only, name)Apply pending migrations to a database (PostgreSQL or SQLite). This is the shared pre-flight for every backend; the backend-specific execution step is dispatched to _run_locked_lifecycle (advisory lock on PostgreSQL, direct on SQLite).
Lifecycle
- Validate: check change_db, install configured extensions, load plan, detect destructive ops
- Confirm: destructive guard + interactive confirmation (TTY-aware — see
_confirm_migration) - Execute: run SQL in a transaction (under an advisory lock on PostgreSQL)
- Record: insert history into pormg_migrations
- Archive: move files to applied_migrations/
Keywords
interactive::Bool=true: prompt for confirmation before applying — only when stdin is a real terminal. In a non-interactive process (CI,Pkg.test, deploy script) no prompt is shown andmigrate()never blocks onreadline().destructive::Bool=false: must betrueto allow DROP TABLE / DROP COLUMN operations. A destructive plan in a non-interactive context throwsDestructiveMigrationErrorunless this is set.dry_run_only::Bool=false: iftrue, only analyze without applying (returns DryRunResult)name::String="pending_migration": name for this migration in the history table
PormG.Migrations.migrate_to — Method
migrate_to(connection, settings, target_version; interactive, destructive)Apply pending migrations up to (and including) a specific version. Only meaningful when multiple migration files exist in the pending queue. For now, this validates the target version against the current history.
PormG.Migrations.remove_migration_record — Method
remove_migration_record(connection, settings, version)Remove a migration record from the history table entirely. Use with caution — this erases history. Intended for cleanup after manual rollbacks or test scenarios.
PormG.Migrations.status — Method
status(connection, settings) -> MigrationStatusReport migration status: applied, failed, pending, and drift signals. Includes basic drift detection by comparing live database tables against the history of applied migrations.
PormG.Migrations.total_statements — Method
Total number of SQL statements in the dry-run result.
PormG.Configuration.close_pool! — Method
close_pool!(pool::Union{PormGPostgres, PormGSQLite})
close_pool!(db::String)Close all connections in the database pool.
PormG.Configuration.current_transaction_depth — Method
current_transaction_depth() -> IntReturn the current transaction nesting depth (0 when not inside any transaction). The outermost run_in_transaction/atomic block is depth 1; each nested savepoint block increments it. Used to derive deterministic, per-level savepoint names (#26).
PormG.Configuration.ensure_before_connect! — Method
ensure_before_connect!(pool) -> BoolRun the registered before_connect hook before opening a physical connection. When no hook is registered, connection setup proceeds normally.
PormG.Configuration.env — Method
env() :: StringReturns the current environment.
Examples
julia> Configuration.env()
"dev"PormG.Configuration.get_tx_connection — Method
get_tx_connection() -> Union{Nothing, <driver connection>}Get the current transaction connection if we're inside a transaction context. Returns nothing if not in a transaction.
PormG.Configuration.in_transaction_context — Method
in_transaction_context() -> BoolCheck if we're currently inside a transaction context.
PormG.Configuration.is_loaded — Method
is_loaded(path_or_key::String) -> BoolReturn true when a static configuration folder or dynamic connection key has already been registered in the in-memory configuration cache.
PormG.Configuration.load_many — Method
load_many(paths::AbstractVector{<:AbstractString}; env::Union{Nothing,String} = nothing)Load several static configuration folders using the same environment override. Returns the list of connection keys that were loaded.
PormG.Configuration.ping — Method
ping(path_or_key::String) -> BoolCheck whether a loaded database configuration is reachable right now. This performs a real connection acquisition and liveness check instead of only verifying that settings exist in memory.
PormG.Configuration.register_connection — Method
register_connection(key::String, url::String; adapter::String = "PostgreSQL", pool_size::Int = 3)Register a new database connection pool dynamically using a connection URL. Useful for multi-tenant applications or connecting to dynamic data sources.
PormG.Configuration.set_before_connect_hook — Method
set_before_connect_hook(f::Function)Register a callback invoked before PormG opens a physical database connection. The callback receives (key::String, settings::Settings) and must return true to proceed or false to abort the connection attempt.
It runs only when a new connection must be opened (not on connection reuse) and is invoked outside the pool lock. Decide inside the callback which connections need setup, e.g. basename(settings.db_def_folder) == "db_esus".
Typical uses include VPN setup, credential refresh, or SSH tunnel activation. When no hook is registered, connections proceed normally.
PormG.Configuration.set_connection_resolver — Method
set_connection_resolver(f::Function)Register a callback function to lazily resolve unknown connection keys. The function f should accept a key::String and return:
nothingif the key cannot be resolved.- A
Tupleof(url, adapter, pool_size)or aDictwith those keys.
PormG.Configuration.status — Method
status(path_or_key::String) -> NamedTupleReturn a compact server-oriented status payload describing whether a configuration is loaded and reachable.
PormG.Configuration.unregister_connection — Method
unregister_connection(key::String)Close the connection pool and remove the configuration for the specified key.
PormG.Configuration.with_tx_context — Method
with_tx_context(f::Function, pool, conn)Run f() with conn installed as the ambient transaction connection, so every query inside the block reuses that one connection instead of checking a fresh one out of the pool.
It only binds the connection. BEGIN and COMMIT/ROLLBACK are the caller's job — run_in_transaction issues them around its own with_tx_context block. Call this directly only when you already hold a connection with a transaction open on it; otherwise in_transaction_context reports true while the database is still in autocommit and nothing can be rolled back.
Use run_in_transaction or atomic instead for ordinary work — they acquire the connection (in :write mode on SQLite), issue BEGIN IMMEDIATE/BEGIN, commit or roll back, and release it:
run_in_transaction("db_sl") do
in_transaction_context() # true — and really inside a transaction
PormG.current_transaction_depth() # 1; a nested atomic() block reports 2
M.Status.objects.create("status" => "Heat shield fire")
endThe context nests: depth increments by one per block (so current_transaction_depth drives savepoint naming), and a nested block inherits the outer block's SQLite reserved-primary-key reservations rather than starting a fresh table. Being a ScopedValue it is dynamically scoped — tasks spawned inside the block inherit it, and it unwinds automatically, including on a throw.
PormG.ConnectionPool.FetchTask — Type
FetchTaskA wrapper around an async database query result that manages connection lifecycle. Use await_result(task) to get the result and properly release the connection.
Fields
async_result: The underlying async handle (aLibPQ.AsyncResultfor PG, aTaskfor SQLite)pool::Union{PormGPostgres, PormGSQLite}: The connection pool to release the connection toconn: The driver connection being used for this querycompleted::Bool: Whether the async result has been awaitedresult_cache::Union{Nothing, Any}: Cached result for multipleawait_resultcallsin_transaction::Bool: Whether this task is part of a transaction (don't release connection)abandoned::Bool: Whether the await was cut short by a cancellation rather than a database failure (#315). Set byawait_result; it sends the connection through the abandoned-await recovery — cancel, wait for the driver, drain, then release or renew — instead of a plain release. (Plain backticks, not an@ref: this struct is exported, so@autodocspublishes this docstring toapi.md, and a cross-reference from there to an internal helper cannot resolve —Private = falsemeans the target is never rendered. That broke the docs build once.)
PormG.ConnectionPool.PoolConnectError — Type
PoolConnectError <: ExceptionThrown by acquire_connection when it could not open a physical connection — a permanently-bad connection string (wrong password, missing role/database, an unopenable SQLite path). Distinct from PoolTimeoutError (a healthy pool that is merely saturated): the remedy here is to fix credentials / host / database, not to raise pool_size (#72). Carries the underlying driver exception (cause) and a redacted connection string; catchable so apps can translate it distinctly (e.g. a 500, not a 503-retry).
Reparented from Exception to PormGError (#239), so catch PormGError covers connection failures too. Catching PoolConnectError specifically is unaffected.
PormG.ConnectionPool.PoolTimeoutError — Type
PoolTimeoutError <: ExceptionThrown by acquire_connection when no connection becomes available within the retry/timeout budget — i.e. the pool is saturated at its ceiling (pool_size * POOL_EXPANSION_FACTOR). It is a catchable Exception (apps can e.g. translate it to a 503 / back off and retry). Remedy: raise pool_size in connection.yml.
Reparented from Exception to PormGError (#239), so catch PormGError covers connection saturation too. Catching PoolTimeoutError specifically is unaffected.
Base.fetch — Method
fetch(connection::Union{PormGPostgres, PormGSQLite}, sql::String; params=nothing, ignore_tx=false) -> Tables.rowtableExecute a database query synchronously (blocking). Internally uses async execution but immediately awaits the result.
PormG.ConnectionPool.acquire_connection — Method
acquire_connection(pool::PormGPostgres; timeout_seconds=nothing, max_retries=300)
acquire_connection(pool::PormGSQLite; timeout_seconds=nothing, max_retries=300, mode=:any)Lease a connection from the pool. You own it until you give it back — every acquire_connection must be paired with a release_connection, and the release belongs in a finally so an exception cannot leak the slot:
conn = acquire_connection(pool)
try
# … use conn …
finally
release_connection(pool, conn)
endMost code should not call this at all — run_in_transaction and the fluent terminals do the pairing for you. Reach for it only when hand-rolling a connection lifecycle.
Keyword arguments
timeout_seconds: how long to wait for a free connection. Defaults to the pool'spool_timeoutfromconnection.yml(30 s if unset, #126); passing it explicitly wins.max_retries: a safety bound on scan/materialize iterations, not a poll count — waiting is event-driven (#124), so this is normally 1.mode(SQLite only)::read,:write, or:any. When the pool has read/write splitting enabled, only the writer slot may write, so a connection you intend to write on must be acquired withmode = :write;:reador:anycan hand you a read-only handle and the write will fail. Any other symbol raisesInvalidValueError.
The pool grows lazily up to pool_size × 10. Exhausting that budget within the timeout raises PoolTimeoutError; a permanent connect failure (bad credentials, missing database, unopenable SQLite file) fails fast with PoolConnectError, whose connection string is redacted. Both are catchable and exported.
See also release_connection, pool_stats, and the Advanced Configuration guide for pool tuning and leak detection.
PormG.ConnectionPool.atomic — Method
atomic(f::Function, db; durable::Bool=false) -> resultRun f() in a database transaction — the friendly, Django-flavored alias for run_in_transaction. db may be a pool, a db-key String (e.g. "db_2"), or a PormGSettings.
A nested atomic/run_in_transaction block on the same database automatically becomes a SAVEPOINT: if f() throws, only that inner block is rolled back to its savepoint and the error propagates, leaving the outer transaction intact (catch it outside the inner block to continue). Works identically on PostgreSQL and SQLite (#26).
atomic("db_2") do
driver = M.Driver.objects.create("forename" => "Alice", "surname" => "Lane", ...)
try
atomic("db_2") do # nested → SAVEPOINT
M.Result.objects.create(...) # rolled back to the savepoint on error…
error("validation failed")
end
catch
# …outer transaction still usable here
end
endPass durable=true to require this block be the outermost transaction — it throws if a transaction is already active (mirrors Django's atomic(durable=True)).
PormG.ConnectionPool.await_result — Method
await_result(ft::FetchTask) -> resultAwait the completion of an async fetch task and return the result. Automatically releases the connection back to the pool.
This function is idempotent - calling it multiple times returns the same result without re-releasing the connection.
PormG.ConnectionPool.fetch_async — Method
fetch_async(connection::PormGPostgres, sql::String; params=nothing) -> FetchTaskStart an async database query that yields to the Julia scheduler. Returns a FetchTask that can be awaited with await_result().
This is useful in Genie.jl async handlers where you want to run multiple queries in parallel or allow other tasks to run while waiting for the database.
Example
# Start multiple queries in parallel
task1 = fetch_async(pool, "SELECT * FROM users")
task2 = fetch_async(pool, "SELECT * FROM orders")
# Both queries run concurrently, await results
users = await_result(task1)
orders = await_result(task2)PormG.ConnectionPool.fetch_copy — Method
fetch_copy(connection::PormGPostgres, sql::String, data_itr)Execute a PostgreSQL COPY FROM STDIN operation using an iterable of data chunks. The driver-specific streaming (LibPQ.CopyIn + result drain) lives in the PostgreSQL extension as backend_copy_in!.
PormG.ConnectionPool.finalize_transaction_connection! — Method
finalize_transaction_connection!(pool, conn; rollback_error=nothing) -> NothingTerminal step of a manually-driven BEGIN/COMMIT/ROLLBACK lifecycle: return conn to the pool exactly once. Pass rollback_error=nothing when the COMMIT succeeded or the cleanup ROLLBACK ran cleanly. If rollback_error is supplied — the cleanup ROLLBACK threw — and it is not a benign "no transaction is active" error, conn may still hold an open/aborted transaction that the acquire liveness probe cannot detect, so it is renewed or discarded instead of released (#71).
Call this from a single terminal finally, exactly as run_in_transaction does, so a lifecycle never releases its connection to the pool before its ROLLBACK has run on it — the release-then- rollback use-after-release race of #139. Never throws: it runs while the transaction's original error is propagating.
Pass renew=true when the lifecycle mutated per-connection session state that releasing cannot undo — today only SQLite's PRAGMA foreign_keys = OFF, which migrations and without_foreign_keys use to suspend enforcement (#276). Renewal re-runs the driver's connect path, which sets the pragma back ON by construction, and it is the renewed handle that returns to the slot; the suspended one is closed. Restoring the pragma with a statement instead would be unsound: PRAGMA foreign_keys is silently ignored while a transaction is open, so a failed COMMIT and failed ROLLBACK would leave enforcement off with the restore reporting success.
PormG.ConnectionPool.pool_stats — Method
pool_stats(pool) -> NamedTupleA snapshot of connection-pool health (#127), driver-agnostic. Returns (; pool_size, size, in_use, available, ceiling, waiting):
pool_size— the configured base floor (warm minimum).size— slots allocated so far (grows lazily under load;== in_use + available).in_use— connections leased right now.available— free slots (idle handles or not-yet-materialized slots).ceiling— the maximum the pool can grow to (pool_size * 10).waiting— callers currently parked waiting for a connection (#124).
Safe to call anytime; all counts — including waiting — are read in one pool.lock critical section for a coherent snapshot (waiter vectors are only ever mutated under pool.lock, #124). PormG's top module adds a pool_stats(key::AbstractString) convenience overload. Unlike close_pool!(db::String) — a teardown that tolerates a never-built pool — that overload throws an ArgumentError for a never-built pool: a zeroed snapshot would read as a healthy empty pool.
PormG.ConnectionPool.release_connection — Method
release_connection(pool, conn) -> BoolReturn a connection leased by acquire_connection to its pool. The other half of the pairing contract — call it from a finally.
Returns true when the slot was found and freed, false (with a warning) when it was not, which means the connection had already been replaced after a failure. The slot is matched by object identity, so you must release the same handle you were given: after a renewal, pass the renewed connection, not the original.
On release the pool either hands the slot straight to a caller waiting for one (#124) or marks it available; an overflow connection past its max_lifetime is closed and retired instead of reused (#125).
A connection whose ROLLBACK itself threw can still hold an open transaction, and this function would hand it back to the pool as-is. Terminate a manual BEGIN/COMMIT/ ROLLBACK lifecycle with finalize_transaction_connection!, which renews or discards it in that case (#71).
See also acquire_connection.
PormG.ConnectionPool.run_in_transaction — Method
run_in_transaction(f::Function, pool::PormGPostgres) -> resultExecute a function within a database transaction with proper connection context. All fetch() calls within the function will automatically use the same connection.
This is the recommended way to run transactions as it ensures:
- All queries use the same connection (transaction isolation)
- Automatic COMMIT on success
- Automatic ROLLBACK on error
- Connection is properly released back to the pool
Example using PormG objects
include("db/models.jl")
import .models as M
result = run_in_transaction(pool) do
driver_query = M.Driver |> object
new_driver = driver_query.create(
"driverref" => "alice_lane",
"code" => "ALA",
"forename" => "Alice",
"surname" => "Lane",
"dob" => "1998-04-12",
"nationality" => "British",
"url" => "https://example.com/alice_lane"
)
race_query = M.Race |> object
race_query.create(
"year" => 2025,
"round" => 1,
"circuitid" => 1,
"name" => "Gran Turismo",
"date" => "2025-03-16",
"url" => "https://example.com/gran_turismo"
)
# Keep counting or aggregating inside the transaction if needed
driver_count = driver_query.count()
return (new_driver[:driverid], driver_count)
endEvery column above is one the F1 models declare null = false; omitting any of them raises at create() before the statement is built.
PormG.ConnectionPool.with_savepoint — Method
with_savepoint(f::Function, settings::PormGSettings, name::String) -> resultExecute f() wrapped in a savepoint named name. On success, releases the savepoint. On error, rolls back to the savepoint, releases it, and rethrows so the outer transaction remains usable.
Works on both PostgreSQL and SQLite — the SAVEPOINT / RELEASE SAVEPOINT / ROLLBACK TO SAVEPOINT statements are identical on both backends (#26). Transparently no-ops when called outside an active transaction context on settings' pool, so callers do not need to guard the call site.
name must be a fixed, non-user-controlled identifier (it is interpolated into the SQL, not parameterized — savepoint names are identifiers). Internal callers pass constants; the reentrant atomic/run_in_transaction path passes _savepoint_name(depth).
PormG.ConnectionPool.with_transaction — Method
with_transaction(pool, sql::String; conn=nothing, release_conn=false, params=nothing) -> (result, conn)Run one statement on a transaction-carrying connection. The building block behind manual BEGIN / COMMIT / ROLLBACK sequences.
It acquires, commits, rolls back and releases correctly on every path. Reach for with_transaction only when you genuinely need to drive the lifecycle statement by statement.
Returns a tuple (result, conn), not just the result — conn is the connection the statement ran on, which you pass back in as conn for the next statement of the same transaction.
Keyword arguments
conn: an existing connection to reuse. Whennothing, one is acquired — on SQLite withmode = :write, since a transaction writes.release_conn: return the connection to the pool when this call finishes. Leave itfalsewhile the transaction is still open; you receiveconnback in the return tuple.params: bound query parameters. Never interpolate values intosql.
On error the connection is never orphaned: it is released if this call acquired it, and a transaction-ending ROLLBACK that itself failed causes a renew-or-discard instead, so a connection with an open or aborted transaction cannot go back into the pool (#71). The underlying driver exception is rethrown as a DatabaseError subtype.
An await cut short by a cancellation (Ctrl+C) takes neither of those paths: the driver may still be on the connection, so it is handed to the abandoned-await recovery — cancel, wait for the driver to let go, then renew — which runs detached and returns the slot only once it is safe (#322).
It releases the connection even when the statement fails, which can hand it back to the pool before your cleanup ROLLBACK runs on it (#139). Do the cleanup on the still- leased connection and return it exactly once from a single finally via finalize_transaction_connection!.
See also acquire_connection, with_transaction_async, and the Transactions and run_in_transaction guide.
PormG.ConnectionPool.with_transaction_async — Method
with_transaction_async(pool::PormGPostgres, sql::String; ...) -> (task, conn)Start an async transaction query. Returns the async handle and connection. The connection is NOT released - caller must manage it for transaction continuation.
For transactions, you typically want to keep the connection for multiple queries.
PormG.ConnectionPool.without_foreign_keys — Method
without_foreign_keys(f::Function, db; check_on_exit::Bool = true) -> resultRun f() in a transaction with foreign-key enforcement suspended, on a single pinned connection.
Try a plain atomic first. Inside a transaction PormG already defers foreign-key checks to COMMIT on both backends, so a block that is transiently inconsistent — writing children before their parents — commits without any special handling. Reach for this only when that is not enough: a load too large for one transaction, a repair that must leave a violation in place, or a test that plants one deliberately.
db may be a pool, a PormGSettings, or a db-key String, like atomic. Every query inside f() reuses the pinned connection, and a nested atomic becomes a SAVEPOINT.
This block must be the outermost transaction on its pool. It cannot nest inside run_in_transaction/atomic and raises TransactionError if you try: suspension works by setting PRAGMA foreign_keys = OFF, which SQLite silently ignores while a transaction is open, so a nested call could not suspend anything.
With check_on_exit = true (the default) a PRAGMA foreign_key_check runs before COMMIT and rolls the block back if it finds an orphaned row, raising UnsafeMutationError — so the escape hatch cannot quietly commit a corrupt database.
Note the check is whole-database, not scoped to what f() touched: on a database that already contains orphans, it will abort a block that did nothing wrong. That is deliberate (it is the same PRAGMA foreign_key_check the migration rebuild gate uses, and narrowing it would mean guessing which rows the block touched), but it means a repair run against an already-inconsistent database wants check_on_exit = false — as does deliberately planting a violation.
The connection is renewed, not returned as-is, so a suspended handle can never serve another caller (#276).
Example
# A load too large to hold in one transaction: commit each chunk, tolerating the inconsistency
# between them, and let the exit check prove the finished result is sound.
without_foreign_keys(pool) do
for chunk in Iterators.partition(eachrow(results_df), 50_000)
bulk_insert(M.Result, DataFrame(chunk))
end
bulk_insert(M.Race, races_df)
end
# For a merely transient inconsistency, a plain transaction is enough — and cheaper, since it does
# not have to renew the connection afterwards:
atomic(pool) do
bulk_insert(M.Result, results_df) # children
bulk_insert(M.Race, races_df) # parents — checked at COMMIT, on both backends
endSQLite genuinely suspends enforcement for the block. On PostgreSQL there is no equivalent — this issues SET CONSTRAINTS ALL DEFERRED, which defers checks to COMMIT rather than skipping them, so a violation still surfaces, just later. check_on_exit is SQLite-only.
See also atomic, run_in_transaction.
PormG.Utils.@import_models — Macro
@import_models(path, alias)Includes a model file and automatically registers the defined module with PormG.
The file at path must define its own module (e.g., module my_models ... end). The alias must match the module name defined in the file.
The macro automatically:
- Resolves the path relative to the calling file
- Registers models for the current session
- Injects an
__init__()function for post-precompilation re-registration - Handles World Age issues in Julia 1.12+
Users do NOT need to manually add an __init__() function to their model files.
Example
# In your package's main module:
PormG.@import_models "db/models.jl" models
import .models as M
# Now use M.Driver, M.Result, etc.
# Models are automatically available here and after precompilationPormG.Utils.@models_module — Macro
@models_module(name, path, block)Defines a new module with inline model definitions and registers it with PormG.
Use this when you want to define models directly in code rather than in a separate file.
Example
@models_module MyDB "db" begin
Driver = Models.Model("drivers",
id = Models.IDField(),
name = Models.CharField()
)
end