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:

MethodDescriptionExample
.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(0) returns no rows, as in SQL; .limit(nothing) removes a limit. A negative value or a Bool raises QueryBuildError..limit(10)
.offset(n)Skip the first n rows. A negative value or a Bool raises QueryBuildError..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. Integers only: a negative value or a Bool raises QueryBuildError, and nothing is .limit()'s alone..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; join_type)Add predicates to the ON clause of a join path, joining it if nothing else in the query does. Does not change the join type unless join_type is passed..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. model is the model object, or its name as a String..cjoin_on(M.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, of)SELECT … FOR UPDATE row lock (PostgreSQL and MySQL, where no_key raises BackendCapabilityError; no lock clause on SQLite; must run inside a transaction on every engine). of limits it to the named tables..select_for_update(of = ("self",))
.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.

Important

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:

MethodReturn TypeDescription
.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)StringReturns results as a JSON string. JSON.json on the rows from .list() produces the same string. A DurationField reads as "00:01:49.088" and a DecimalField as a JSON number carrying its exact digits — see Serializing rows to JSON.
.list(T)Vector{T}The typed read (#1352): each row built into a struct or NamedTuple type T you declare, so the code after the query is type-stable. Projects fieldnames(T) when there is no values(...); a field with no column or a column with no field is a QueryBuildError, and a value whose type T does not take an InvalidValueError. See Typed rows.
query |> DataFrameDataFramePipe to DataFrame (after using DataFrames) for tabular output. A query is a Tables.jl table, so any Tables.jl sink reads it the same way (CSV.write(io, query), Tables.rowtable(query)); each read executes the query once. Temporal columns are typed (Date, Time, ZonedDateTime, CompoundPeriod) on every engine, as in .list().
.count()IntRuns SELECT COUNT(*) and returns the count.
.aggregate(alias => Agg(...), ...)NamedTupleWhole-queryset aggregation (no GROUP BY); returns one named tuple of scalars. See Aggregation.
.exists()BoolReturns true if at least one row matches.
.first()PormGRow or nothingReturns the first matching record or nothing.
.last()PormGRow or nothingReturns the last matching record; inverts order_by, or falls back to primary-key descending when unset.
.earliest(fields...)PormGRowEarliest row ordered by fields (ascending; "-field" flips); raises DoesNotExist when empty.
.latest(fields...)PormGRowLatest row ordered by fields (descending; "-field" flips); raises DoesNotExist when empty.
.get(filters...)PormGRowReturns exactly one row, or raises DoesNotExist / MultipleObjectsReturned.
.first(T), .last(T), .get(T, filters...), .earliest(T, fields...), .latest(T, fields...)T (first/last: or nothing)The single-row terminals as typed reads, under .list(T)'s rules.
.create(key => value, ...)PormGRowInserts 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()DictFull query metadata without executing — the inspect_query shape (see Query Inspection & Debugging below).
.explain(; analyze, buffers, verbose)DictThe database's plan for the SELECT — the explain_query shape (see Query Inspection & Debugging below). analyze = true executes the query.

On a query with .limit() or .offset() set, .first(), .get(), .exists() and .count() work inside the slice, while .last(), .earliest(), .latest(), .count("column") and .aggregate() raise QueryBuildError — see Read terminals on a sliced query.

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:

MethodReturn TypeDescription
row.fieldvalueReads a selected field using normalized Julia-style field names.
row[:field]valueReads a selected field by Symbol or String.
row.pkvalueThe 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)valueFunction form (exported by using PormG); the 2-arg variant returns default instead of throwing (best-effort).
row.relationshipManyToManyManagerAccesses a many-to-many relationship manager when the model defines one.
row.save()PormGRowPersists fields assigned on the row and clears its dirty state.
row.save(show_query=:sql)VectorReturns 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.

ModeDescription
:executeDefault. Executes the query and returns results.
:sqlReturns the SQL string only. Minimal overhead for benchmarking.
:prettyReturns the SQL string reflowed one clause per line, for logs and the REPL. Only whitespace between tokens changes, so it runs exactly as :sql does.
:dictReturns the metadata dictionary — the eight keys below.
:inspectionAlias of :dict. Useful when you want the same rich metadata shape used by inspect_query().
:paramsReturns the parameters array only.
:structReturns, as a String, the struct a typed read (list(T)) accepts for the query: using lines, a header naming the engine, one field per projected column. Read terminals only (list, first, last, get, earliest, latest, query_list); see Typed rows.
:noneReturns nothing, or a Vector of nothings for a call that runs several statements (a cascading delete()). 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)

# Readable SQL for a log line
@info "drivers query" sql = query.list(show_query=:pretty)

:dict / :inspection return the same eight keys for every operation — a read, count/exists, create, update, each delete step and each bulk form:

KeyValue
:sql_textThe SQL string — exactly what executes (on PostgreSQL, create() includes its RETURNING of the model's columns).
:parametersThe bound values, in bucket order.
:parameter_countlength(:parameters).
:parameter_bucketsSQLite and MySQL: the values per positional bucket (:select, :where, :limit, …). PostgreSQL: empty.
:dialect:postgresql, :sqlite or :mysql.
:bucketing:numbered ($1, PostgreSQL) or :positional (?, SQLite and MySQL).
:modelThe model / table name.
:operation:select, :insert, :update or :delete; a cascading delete also has :lock steps on PostgreSQL and MySQL, and a filtered one starts with its key read (:lock there, :select on SQLite); bulk_copy reports :bulk_copy.

A statement that runs in several steps — a cascading delete(), a chunked bulk call — returns a Vector of these, one per step.

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, :sqlite or :mysql
Note

LIMIT and OFFSET values are bound parameters like every other value (#46), so one statement text serves every page size. They come last in inspection[:parameters] — LIMIT, then OFFSET — and on SQLite and MySQL they sit in their own inspection[:parameter_buckets][:limit] bucket. An offset with no limit renders LIMIT -1 OFFSET ? on SQLite and LIMIT 18446744073709551615 OFFSET ? on MySQL, whose grammars have no standalone OFFSET; PostgreSQL keeps the bare OFFSET $N.

explain_query

The database's plan for the SELECT a query builds — Django's QuerySet.explain(). It is read-only: it explains the statement show_query(q) renders, and writes are terminals, not something a query can be explained as. The fluent form is .explain().

plan = M.Result.objects.
    filter("driverid__surname" => "Senna").
    values("raceid", "points").
    explain()

plan[:indexes_used]    # ["result_driverid_lunjedbf_idx"]
plan[:seq_scans]       # ["driver"]
plan[:total_cost]      # 167.35   (the engine's estimate on PostgreSQL and MySQL; nothing on SQLite)
plan[:estimated_rows]  # 62

The result is the inspect_query dictionary plus these keys, present on every engine:

KeyPostgreSQLSQLiteMySQL
:explain_sqlThe EXPLAIN (FORMAT JSON …) statement that ran.The EXPLAIN QUERY PLAN statement that ran.The EXPLAIN FORMAT=JSON statement that ran.
:planThe parsed JSON plan document.The plan rows, as Dicts of :id, :parent, :detail.The parsed JSON plan document.
:indexes_usedEvery Index Name in the plan tree.Each USING [COVERING] INDEX name; "INTEGER PRIMARY KEY" / "PRIMARY KEY" for a key lookup; "AUTOMATIC INDEX" for a transient index SQLite builds for the one statement.Every table's key; "PRIMARY" for the primary key.
:seq_scansEach table read by a Seq Scan.Each SCAN step that uses no index — named by the query's alias (Tb, Tb_1, …).Each table read in full (access_type ALL), named by the query's alias.
:total_cost, :estimated_rowsThe root node's estimates.nothingThe query block's cost, and the rows its last table produces.
:planning_time_ms, :execution_time_msMeasured, with analyze = true.nothingnothing
:analyzeWhether the query was executed.falsefalse
KeywordEffect
analyze = falsetrue runs EXPLAIN ANALYZE, which executes the query to measure it.
buffers = falseAdds PostgreSQL's buffer usage to :plan.
verbose = falseAdds PostgreSQL's VERBOSE detail to :plan.

SQLite has only EXPLAIN QUERY PLAN, and MySQL's EXPLAIN ANALYZE prints only a text tree: passing any of the three keywords on either raises BackendCapabilityError.

A select_for_update() query needs a transaction to be explained, on every engine — with or without analyze — exactly as it does to run, and raises QueryBuildError outside one.

The facts are what the plan states — PormG does not warn about a "missing" index. A sequential scan is often the right plan (a small table, or a filter that matches most rows), and the planner already weighed the indexes it has.


Filter Operators

PormG uses __@ suffixes for lookup operators and field transforms:

Comparison Operators

OperatorSQL EquivalentExample
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__@inIN (...)"nationality__@in" => ["British", "French"]
field__@ninNOT IN (...)"nationality__@nin" => ["British", "German"]
field__@rangeBETWEEN a AND b"driverid__@range" => [1, 50]
field__@isnullIS NULL / IS NOT NULL"dob__@isnull" => true
field__@containsLIKE '%val%'"name__@contains" => "Monaco"
field__@icontainsILIKE '%val%'"name__@icontains" => "monaco"
field__@startswithLIKE 'val%'"surname__@startswith" => "Ver"
field__@istartswithILIKE 'val%'"surname__@istartswith" => "ver"
field__@endswithLIKE '%val'"surname__@endswith" => "sen"
field__@iendswithILIKE '%val'"surname__@iendswith" => "SEN"
field__@iexactLOWER(col) = LOWER(val)"surname__@iexact" => "HAMILTON"

Every pattern lookup above has a negated twin (@ncontains, @nistartswith, @niexact, …). There are also two PostgreSQL-only pairs: the accent-insensitive @iunaccent_contains / @iunaccent_exact, and the POSIX regular-expression @regex / @iregex. Each has a negated twin as well (@niunaccent_*, @nregex / @niregex). The full table, with NULL semantics and indexing notes, is in Filters and Aggregates.

An ArrayField (PostgreSQL only) takes @acontains (@>), @contained_by (<@) and @overlap (&&) with a Vector value, an index ("tyre_compounds__0", 0-based) and a slice ("pit_laps__0_2"). See Filters and Aggregates → Array Lookups.

field__@search (PostgreSQL only) is full-text search on a text column: "surname__@search" => "senna", or => SearchQuery("senna"; config = "simple") for a config and a search type. See Full-Text Search.

Transform Functions

TransformDescriptionExample
field__@yearExtract year from date"dob__@year" => 1960
field__@monthExtract month (1-12) from date"dob__@month" => 3
field__@dayExtract day (1-31) from date"dob__@day" => 21
field__@quarterExtract quarter (1-4)"date__@quarter" => 1
field__@quadrimesterExtract quadrimester (1-3)"date__@quadrimester" => 2
field__@yyyy_qYear-qualified quarter label ("YYYY-Qn")"q" => "date__@yyyy_q"
field__@yyyy_quadYear-qualified quadrimester label ("YYYY-Qn")"q" => "date__@yyyy_quad"
field__@dateExtract date from datetime"created_at__@date" => Date(2025, 1, 1)
field__@hourExtract hour (0-23)"start_at__@hour" => 13
field__@minuteExtract minute (0-59)"start_at__@minute" => 30
field__@secondExtract whole second (0-59)"time__@second" => 0
field__@weekISO-8601 week (1-53)"date__@week" => 10
field__@week_dayDay of the week, 1 = Sunday … 7 = Saturday"date__@week_day" => 1
field__@iso_week_dayISO day of the week, 1 = Monday … 7 = Sunday"date__@iso_week_day" => 7
field__@iso_yearISO-8601 week-numbering year"date__@iso_year" => 2021
field__@lenElement count of an ArrayField (PostgreSQL only)"pit_laps__@len__@gte" => 2

A lookup chains after a transform: "date__@year__@range" => [1990, 1999], "sprint_date__@year__@isnull" => false. A transform is NULL exactly when its column is; see Null checks and ranges after a transform.

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:

FunctionSQLExample
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") |> DataFrame

SQL Functions

PormG.Functions provides a comprehensive set of SQL functions:

String Functions

FunctionDescriptionExample
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 text; a NULL operand is skipped, and a non-text one (a Bool, Float, Decimal, timestamp, interval or JSON document) is refused on every engine: cast it to text first"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", "-", " ")
LPad("field", len, fill = " ")Pad on the left; the column must be text"car" => LPad(Cast("number", "text"), 3, "0")
RPad("field", len, fill = " ")Pad on the right; the column must be text"code" => RPad("code", 5, ".")
Substr("field", pos, len = nothing)len characters from position pos (from 1), or the rest of the string; the column must be text"first3" => Substr("surname", 1, 3)

Numeric Functions

FunctionDescription
Abs("field")Absolute value
Round("field", precision)Round to precision places with each engine's own ROUND; a decimal tie can differ in its last digit on SQLite (#1061); over an integer it is that integer (#1196). Text raises; a negative precision rounds to tens on PostgreSQL and MySQL and raises BackendCapabilityError on SQLite (#1167)
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

FunctionDescriptionExample
Value(x)Literal value in SQLValue("hello")
Coalesce(args...)First non-null value (two or more arguments)Coalesce("nickname", "forename")
NullIf("field", value)Returns NULL if equalNullIf("code", Value(""))
Greatest(args...)Maximum of values (two or more arguments)Greatest("points", 0)
Least(args...)Minimum of values (two or more arguments)Least("points", 100)
Cast("field", type)Type casting — a field object or a type string. A cast to text, an integer (#1028) or a scaled numeric(p, s) (#1040) raises QueryBuildError on an engine that would compute a different value from what it means (#1147)Cast(Round("points"), IntegerField()), Cast("points", "numeric")
Extract("field", "part")Extract date/time partExtract("dob", "year")
ToChar("field", fmt)Format to stringToChar("dob", "YYYY-MM")

Full-Text Search Functions (PostgreSQL)

FunctionDescriptionExample
SearchQuery(text; config, search_type)The query: plainto_tsquery, phraseto_tsquery, websearch_to_tsquery or to_tsquery; &, | and ~ combine queries"surname__@search" => SearchQuery("senna"; config = "simple")
SearchVector(fields...; config, weight)The document: the fields cast to text, NULL-safe, joined by a space, labelled "A"–"D" by weight; + adds twoSearchVector("surname"; weight = "A") + SearchVector("forename"; weight = "D")
SearchRank(vector, query; normalization, cover_density, weights)ts_rank / ts_rank_cd, as a Float64; weights are D, C, B, A"rank" => SearchRank(SearchVector("forename", "surname"), "senna")
SearchHeadline(field, query; config, start_sel, stop_sel, …)ts_headline: the text with the matched words marked"hl" => SearchHeadline("name", "grand prix"; start_sel = "<b>", stop_sel = "</b>")

Each raises BackendCapabilityError on SQLite and MySQL when the query is built. The config, the search types and the options are in Full-Text Search. To index a search, declare the expression with Models.search_vector_expression(columns...; config), which returns the to_tsvector(…) text the query renders (see Indexing on that page). To search several columns, project a SearchVector under a name and filter it: values("doc" => SearchVector("forename", "surname")).filter("doc__@search" => "lewis").

An operand of Coalesce, Greatest, Least, NullIf, Power, Mod and Concat is read by its type. A string is a column path. A number, a Bool, or a date or time (Date, DateTime, ZonedDateTime, Time) is a literal that is bound as a parameter, so Coalesce("points", 0) means Coalesce("points", Value(0)). Wrap a string literal in Value, as in NullIf("code", Value("")). Any other value raises QueryBuildError when the expression is built. Concat is narrower: a Bool, float or Decimal operand, as a literal, a column or an expression, raises QueryBuildError on every engine too, because the query does not say which text it means (#1027), and so does a timestamp, an interval or a whole JSON document (#1028). A zero-scale DecimalField column and a cast to a zero-scale decimal (numeric(p,0)) are whole numbers and pass, except that SQLite raises for such a cast over a value that is not an integer, which it does not round (#1034). Cast such an operand to text first. A cast to text does not refuse them: a float, a decimal, a timestamp, an interval or a JSON document cast to text builds on every engine, written in each engine's own text. A cast is refused only on an engine that would compute a different value from what the cast means (#1147). On SQLite that is a Bool cast to text, a float or a decimal cast to an integer unless it is rounded first with Round, Floor or Ceil, and a cast to numeric(p, s) of a float, text, a numeric function or a decimal with more than s places, since SQLite does not round to the scale (#1040). On MySQL it is a Bool cast to text, a date, a timestamp, a time or an interval cast to a number, and a literal a numeric(p, s) cannot hold (MySQL Guide). An output_field of text, an integer or a scaled numeric on Coalesce, Greatest, Least or Case follows the same rules (#1028, #1040), a Case branch by branch (#1217). A cast an engine computes differently has the full table, including the integer quotient PostgreSQL refuses (#1111).

Case Expressions

Binary (single condition): pass otherwise directly to When — no Case wrapper needed:

# CASE WHEN position <= 3 THEN 'Yes' ELSE 'No' END
"podium" => When("position__@lte" => 3, then = "Yes", otherwise = "No")

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")

A When with no otherwise is only a Case branch: used alone as a value, as in Count(When(…)), it raises QueryBuildError.

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") |> DataFrame

Parameters:

ArgumentTypeDescription
main_joinPair{String,String}"field" => "TargetModel" — the join path.
filtersVectorON-clause predicates. Supports Pair, Q(), Qor(), F expressions.
join_typeString"LEFT" (default), "INNER", "RIGHT", or "FULL". Anything else — "CROSS" included — raises QueryBuildError. "RIGHT" and "FULL" are the right_joins / full_joins rows of the capability table: MySQL has no FULL JOIN, and SQLite has neither before 3.39.0.
fieldPormGFieldOptional custom field definition for non-FK joins.
warnBoolSuppress auto-discovery warnings (default: true).

See Custom Joins for the full documentation.

on()

Adds ON-clause predicates to a join path (including reverse joins) without redefining it. When nothing else in the query reaches the path, on() builds the join itself, so the predicate is never dropped:

query = M.Result.objects
query.on("driverid", "nationality" => "Brazilian", "code" => "SEN")
query.values("resultid", "driverid__surname", "points")

on() adds predicates only. The join keeps the type PormG derives from the relation — the field's own how, else LEFT for a nullable ForeignKey and INNER for a NOT NULL one, and LEFT for a reverse or ManyToMany relation a filter's Qor arm or __@isnull => true reads — unless you pass join_type yourself, which then stays in effect for later on() calls on the same path.

A condition's key names the joined row, and a right-side F names the base row in every spelling ("number" => F("number"), Q(...), OP(...), F(...) == F(...)). See Which row each side of a condition names.


Bulk Operations

bulk_insert

Inserts multiple records in a single operation. Returns (count, rows): count is the rows inserted, summed across chunks (rows skipped by on_conflict = :nothing are not counted), and rows is nothing unless returning= asks for the written values. See Return Value.

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; MySQL as ON DUPLICATE KEY UPDATE, whose target must be the one unique key the rows can conflict on; 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"]))

returning= hands back generated keys and database defaults as a table of the kind you passed (a DataFrame for a DataFrame), aligned row for row with the input — a row the call skipped is missing (see Returning Generated Values):

r = bulk_insert(M.Driver, df; returning = ["driverid"])
df.driverid = r.rows.driverid

bulk_update

Updates multiple records in a single operation. Returns (count, rows): count is the rows matched, summed across chunks, and rows is nothing.

r = bulk_update(M.Result.objects, df_with_changes, columns=["points"], match_on=["resultid"])
r.count   # rows matched; 0 when no key in the frame matched a row in scope

Key contracts:

  • columns= names the participating fields and is the single place a column of the input table 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_on field's source column is its columns= mapping when you declared one — the mapping is authoritative even if a same-named column of the table also exists (that case warns) — otherwise a column of the table with the field's own name. A value PormG auto-populates for an out-of-scope field (auto_now, or a static default when columns= is omitted) is not a declared mapping: your same-named column outranks it, and with no column at all the call raises an UnknownFieldError rather than matching every row against one per-call constant.
  • A match key is matched, never written — it stays out of the SET clause, so using an auto_now field as a key does not refresh that timestamp.
  • The table's columns are matched case-sensitively for both columns and match_on. A column that differs only in case from the model field (e.g. RaceId vs raceid) raises an error naming the candidate; normalize headers with rename!(df, lowercase.(names(df))) or map explicitly with "DF_COL" => "field" in columns=.
  • If match_on is 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 to match_on=; there is no silent fallback. Likewise, the pre-#107 pair grammar in match_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.
  • Filters already attached to the handler are kept: they are AND'd with match_on= and filters=, as update() honors them. The handler itself is never modified — bulk_update() builds from a private copy, and filters= is not written back onto it.
  • A handler carrying limit(), offset(), order_by(), distinct(), an aggregate annotation, a CTE, or a cjoin/on/cjoin_on join raises UnsafeMutationError; a plain values() projection is ignored.
  • 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 0 when that referenced row exists; use nothing or missing to write SQL NULL on 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. Returns (count, rows): count is the rows copied, and rows is nothing.

bulk_copy(M.Driver.objects, df)
FunctionBest ForSpeedProtocol
create()Single rowsStandardSQL INSERT
bulk_insert()Medium datasets (< 10k rows)FastMulti-row INSERT
bulk_copy() ⭐Massive datasetsUltra-FastPostgres COPY
bulk_update()Modifying many rowsFastMulti-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
end

Key features:

  • Async context propagation — spawned @async tasks inherit the transaction
  • Connection sharing — all queries on that database 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 every engine):

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
end

Pass durable = true to require the block be the outermost transaction on its database (raises if one is already active on the same database; a transaction on another database does not count).

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). of limits the lock to the named tables — "self", a relation path the query joins, or a cjoin_on alias — and renders FOR UPDATE OF "<alias>", …. Without it PostgreSQL locks every table in the query, joined ones included, and a lock without of beside an outer join raises QueryBuildError. It must run inside a transaction on every engine. On MySQL it renders the same clause, and no_key raises BackendCapabilityError. On SQLite it renders no lock clause (no row-level locking; the transaction serializes writers), though it still checks the of targets. 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)
end

Async 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() directly

fetch_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 connection

Bind user values with a plain array — write the backend-native placeholder ($1, $2 on PostgreSQL, ? on SQLite and MySQL); 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 and MySQL: the same call with "... = ?" and the same ["Brazilian"] array
An un-awaited `FetchTask` leaks its pool connection

fetch_async checks its connection out synchronously; only await_result returns it. Always await every task you start. (A connection you pass as conn = c is borrowed and is never released for you — see Who releases a connection.)

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, root=nothing, force=false, before_connect=nothing)

Loads a database configuration folder. Use env to explicitly set the environment instead of relying on ENV["PORMG_ENV"]; outside a REPL one of the two is required (#1228).

The adapter's driver package must be loaded first — using LibPQ (or Postgres) / using SQLite / using MySQL — or load raises MissingConfigurationError naming it, at boot rather than on the first query (#1232). before_connect = (key, settings) -> Bool gives this key its own hook, run before a physical connection is opened, in place of the process-wide set_before_connect_hook.

PormG.Configuration.load("db"; env="prod")

A test or a script that renders SQL and never runs a query does not load: it registers the key with Configuration.register_offline (below) instead, which opens no pool (see Testing Your App). A load of a key registered offline raises InvalidConfigurationError, force = true included, rather than replace it with a live pool (#1451). Call unregister_connection(key) first to load it.

Without root, path is both the connection key and the folder, resolved against pwd(). With root, the key is path as passed and the folder is joinpath(root, path), stored absolute in settings.db_def_folder. That gives a short key from any working directory. path must then be relative:

PormG.Configuration.load("db"; root = APP_ROOT, env = "prod")   # key "db"

Returns Union{Nothing, String}: the connection key it registered, or nothing when scaffold=true wrote a skeleton instead of loading anything. The key is normally the string you passed, but a folder already loaded under a different spelling is reused rather than registered twice (#550), so it can differ — hold on to the return value instead of assuming your own spelling:

key = PormG.Configuration.load("db"; env="prod")
settings = PormG.Configuration.get_settings(key)

Loading a folder that is already loaded with the same environment returns its key and does nothing else: the pool and any connection checked out of it are kept, and connection.yml is not read again (#1225). Another env reloads the folder, with a warning. force = true re-reads it and rebuilds the pool regardless.

Configuration.load_many(paths; env=nothing, root=nothing, force=false)

Bootstraps multiple database folders in one call, with the same env and root for each:

PormG.Configuration.load_many(["db", "db_analytics"]; root = APP_ROOT, env = "prod")

Returns the connection keys actually registered. As with load, a folder already held under another spelling is reused, so a returned key is not always the path that was passed — use the returned list rather than the one you supplied.

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",
#  db_def_folder="db", dynamic=false, implicit=false)

Configuration.loaded_keys()

The connection keys PormG has registered — static and dynamic — sorted (#1229). The public way to ask what is loaded, instead of iterating PormG.config:

PormG.Configuration.loaded_keys()   # ["db", "db_telemetry"]

Configuration.register_offline(key; adapter = :postgres, server_version = nothing, django_prefix = nothing, change_data = true)

Registers key as a connection that renders SQL and never connects (#1229), for golden-SQL tests and inspect_query with no database. adapter (:postgres or :sqlite) picks the dialect. Models bind to it like a loaded folder. Running a query on it raises InvalidConfigurationError, a key already registered as a live connection is refused, and a later load of the offline key raises InvalidConfigurationError (#1451):

PormG.Configuration.register_offline("db")
PormG.@import_models "db/models.jl" models
info = PormG.inspect_query(models.Driver.objects.filter("surname" => "Senna"))
info[:sql_text]    # "SELECT\n    *\nFROM \"driver\" as \"Tb\"\nWHERE \"Tb\".\"surname\" = \$1\n"
PormG.Configuration.unregister_connection("db")

Its environment is always "dev", whatever PORMG_ENV says (#1256): an offline entry reads no connection.yml, so there is no environment block to select, and the value is a constant rather than load's fallback.

An offline key needs no driver package. NULLS FIRST/LAST, window functions, RIGHT/FULL joins and the bind-parameter limit of a bulk statement depend on the SQLite version, and an offline SQLite key renders them for its server_version (3_045_000 for 3.45.0), never for the library of the machine that renders it. Without it, a query that needs the version raises InvalidConfigurationError naming server_version (a bulk write always does, whatever its row count); server_version with adapter = :postgres raises the same error, since PostgreSQL's SQL depends on no server version (#1452). A delete(show_query = …) of a model that others reference renders its cascade without reading anything: the statements after the root-key read are scoped by an empty key list, and a PROTECT or RESTRICT relation raises ProtectedError as if related rows existed (#1453). A golden-SQL suite built on it is the subject of Testing Your App.

Configuration.register_connection(key, url; adapter = "PostgreSQL", pool_size = 3, env = nothing, …)

Registers a connection pool at runtime from a connection string — see Dynamic & Multi-Tenancy. adapter = "MySQL" raises InvalidConfigurationError: a MySQL connection is configured by its keys in connection.yml, through load. The environment the entry records, which env(key) and status(key) report, follows load's rule (#1228, #1256): env = when passed, else a non-blank PORMG_ENV, else — in an interactive session only — "dev" with an @info. A service, a test or a script that states neither gets InvalidConfigurationError, before any pool is built or replaced. A connection resolver states it with an "env" entry in the Dict it returns:

PormG.Configuration.register_connection("tenant_monaco", "postgres://user:pass@host/monaco"; env = "prod")
PormG.Configuration.env("tenant_monaco")   # "prod"

Configuration.close_all!()

Closes every registered pool without waiting for connections still checked out; the entries stay registered (#1229). It is what PormG runs at process exit, for a script or a test that wants to release its connections early.

Models.connection_key(model)

The connection key a model is bound to, or nothing before it is bound (#1229). Read this rather than the connect_key field to check which database a model's queries reach:

PormG.Models.connection_key(M.Driver)   # "db"

Models.@models Circuit Race …

The last line of a models module: the bindings of the models it defines, which set_models reads (#1602). PormG does not scan a module's names for models; a module registered after a plain include that writes no @models line raises ModelDefinitionError naming the line to add, and @import_models / @models_module add it from the file's top-level X = Models.Model(…) assignments. The line binds the module's MODEL_SET::Models.ModelSet, which holds the models (it comes after them). A juliac --trim application registers with set_models(models.MODEL_SET; key).

Models.bind!(model, key)

Binds one model to the loaded connection key and does nothing else (#1230): the narrow half of set_models, for a model built at runtime — a library's own infrastructure table — that is not part of a models module. key is accepted when it is registered (load, register_connection, register_offline) or served by the connection resolver (set_connection_resolver), which bind! consults as get_settings(key) does (#1255). Any other key raises InvalidConfigurationError; no folder is loaded for you. It does not register a module, resolve ForeignKey targets given by name, or install reverse accessors.

Migrations.ensure_table!(model, key; indexes = true, force = false)

Creates model's table on key if it does not exist and returns :created, :exists, or :disabled on a change_db: false connection (#1230). force = true creates it there too (#1677): pass it for a table your library owns and keeps out of makemigrations with register_ignore_tables!, since change_db: false is migrate's rule for the application's schema and the usual production setting. It changes nothing else, and never alters or recreates a table that exists. The DDL is what a migration emits for a new model — table, foreign keys, indexes (a PostgreSQL Index(...; opclasses = ("text_pattern_ops",)) included), unique constraints and CHECKs — in one transaction, under the migration advisory lock on PostgreSQL. On MySQL it runs under the migration's GET_LOCK on one connection, with no transaction, and drops the table again when a statement after its CREATE TABLE fails. It never alters an existing table. For infrastructure tables kept out of makemigrations with register_ignore_tables!:

LiveTiming::PormG.Models.ModelType = PormG.Models.Model("live_timing", id = PormG.Models.IDField(),
                                      driverref = PormG.Models.CharField(max_length = 64))
PormG.Models.bind!(LiveTiming, "db")
PormG.Migrations.ensure_table!(LiveTiming, "db")   # :created, then :exists on every later boot
PormG.Migrations.ensure_table!(LiveTiming, "db"; force = true)   # also under change_db: false

Configuration.redact_secret(conn_str)

Masks the credentials in a connection string. This is the single rule PormG applies everywhere a connection string could be seen — log lines, exception messages, REPL display — and it handles both dialects PormG accepts:

julia> PormG.Configuration.redact_secret("host=localhost password=s3cret user=pingo")
"host=localhost password=**** user=****"

julia> PormG.Configuration.redact_secret("postgresql://pingo:s3cret@localhost:5432/f1")
"postgresql://****:****@localhost:5432/f1"

Only the credentials are masked — host, port, database and query parameters survive, so a connect failure still shows where it was pointing. The result is a fixed point: redacting twice changes nothing.

Where the rule is unsure, it masks more rather than less. In a URL it masks everything from :// to the last @ on the line, because a password holding an unencoded / or @ puts part of itself after that character. So a URL whose path or query contains an @ (postgres://localhost/f1?opt=a@b) loses its host and database from the output (postgres://****@b).

Pools and Settings already render and serialize safely on their own (see Credentials never leave through show or JSON.json); call this directly when you want the connection string itself in output you control.

For the full configuration guide, see Configuration.


Advisory Locks

with_advisory_lock

Acquires a PostgreSQL advisory lock, or a MySQL named lock (GET_LOCK), for distributed coordination:

PormG.with_advisory_lock("db", "rebuild_driver_standings"; wait=true, timeout_ms=10000) do
    # Critical section — only one process at a time rebuilds the standings table
    rebuild_driver_standings!(2024)
end

migrate() takes its own lock and needs no wrapper; see Deploying.

Strategies:

  • :poll (default) — Client-side polling with interval
  • :block — Server-side blocking via pg_advisory_lock (PostgreSQL) or GET_LOCK with a timeout (MySQL)

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:

TypeDescription
PormGAbstractTypeBase abstract type for all PormG types.
PormGSettingsThe abstract connection-settings type; Configuration.Settings is its one concrete subtype, and the only one PormG.config holds (#1622). Renamed from SQLConn.
PormGBackendBase for the database backend/dialect markers (subtypes: PormGPostgres, PormGSQLite, PormGMySQL), the dispatch key for SQL rendering and driver selection.
SQLObjectBase for objects that can be stored in the database.
SQLObjectHandlerHandles operations on SQL objects (the query builder).
SQLTableAliasManages table aliases in SQL queries.
SQLInstructionRepresents an instruction to build a SQL query.
SQLTypeBase for SQL-related expression types.
SQLTypeFieldRepresents a field expression in queries.
SQLTypeQQ-expression type (AND logic).
SQLTypeQorQor-expression type (OR logic).
SQLTypeFF-expression type (field references).
SQLTypeFunctionSQL function type.
SQLTypeCTESupertype of CTE's reference object — a CTE column handle.
SQLTypeJoinedSupertype of Joined's reference object — a cjoin_on joined-copy column handle.
PormGModelBase for model types.
PormGFieldBase for field type definitions.
PormGErrorRoot 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, Joined, Interval, show_query, inspect_query, explain_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). To answer a bad request, catch the UsageError umbrella rather than the concrete refusal (#1238):

PormGError, UsageError, FieldAccessError, UnknownFieldError, AmbiguousFieldError, LazyTraversalError, FilterError, QueryBuildError, UnsafeMutationError, InvalidValueError, WritesDisabledError, UnsupportedConnectionError, BackendCapabilityError, ProtectedError, DefinitionError, FieldValidationError, ModelDefinitionError, ConfigurationError, InvalidConfigurationError, MigrationError, InvalidMigrationError, PoolError, DatabaseError, IntegrityError, OperationalError, StatementError, TransactionError, error_message

Database failures are wrapped too

The taxonomy has two halves. UsageError reports PormG refusing the call — almost always 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.* / MySQL.MySQLError (which would mean depending on the driver package just to spell the type). The driver's own exception stays reachable on .cause (#268).

These are not `ArgumentError`s

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, MigrationPrecheckError and PlanPreconditionError (which render their statements, findings and drifted tables 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)
end

error_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

TypeRaised when
UsageError (abstract)Umbrella for "PormG refused the call for what it asked", not because the database failed — FieldAccessError and its three cases, FilterError, QueryBuildError, UnsafeMutationError and InvalidValueError. The remedy is to change the call. Catch it to answer a bad request: it keeps matching when a refusal moves between its members, which a concrete catch does not (#1238). Not members: ProtectedError, BackendCapabilityError, UnsupportedConnectionError, and the DatabaseError family.
FieldAccessError (abstract)Umbrella for field/accessor lookup failures, under UsageError — catch it to get all three cases below.
UnknownFieldErrorA field, alias, column, or __ lookup path does not exist on the model or projected row — or the path ends at a relation (a reverse accessor or a ManyToManyField) instead of a column, the message names the relation and asks for a column after it (#1134).
AmbiguousFieldErrorA name has two meanings on this query. Either a __ path's first segment names both a declared CTE and something on the model: spell the CTE side CTE("<name>", "<path>"), or rename the CTE to reach the model side (#492). Or a filter(...) key, or a condition inside another projection (a When or a Q in a Case), names both a model field and a projection alias that projects something else: rename the alias (#703, #706). Or an order_by(...) path reaches a related column and is also the name PormG generated for a transform projection (values("weekend__@year") over a date-keyed foreign key is named weekend__year): order by "weekend__@year" for the projection, or name it (#1004). A filter or condition on a transform's generated name whose path reaches no column ("date__day" beside values("date__@day")) raises it too: a generated name is not a filter key, so filter "date__@day".
LazyTraversalErrorAn unprojected ForeignKey or OneToOneField was read off a fetched row — project it in values(...) first.
FilterErrorInvalid filter shape — an unknown lookup, a list where one value belongs, or an operator misused on a JSON/subquery column. A value the filter cannot bind is an InvalidValueError (#988).
QueryBuildErrorStructural/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.
UnsafeMutationErrorAn update()/delete() was requested without a filter, or an update()/delete()/bulk_update() on another unsafe shape.
ProtectedErrorA 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.
BackendCapabilityErrorThe active backend cannot do this: PG-only lookups on SQLite (JSONB, iunaccent_*, the regex family), explicit window frame= on SQLite, bulk_copy on SQLite, with_advisory_lock(...; on_missing_lock = :error) on SQLite, a ToChar format outside the portable table on SQLite, a DecimalField wider than the 15 digits SQLite stores exactly (at makemigrations), or a too-old SQLite library. On MySQL: the regex, JSONB, iunaccent_* and full-text lookups, select_for_update(no_key = true), a GROUPS or EXCLUDE window frame, a partial index, migrate of several statements without atomic = false, or a server older than 8.4. Change the query or the backend.
InvalidValueErrorA value failed coercion/type validation on a filter, an insert or an update — the message names where, never the value, and kind/field/row carry it as data (#971) — an identifier failed the safety check, an interval/duration could not be parsed, or a piece of SQL grammar PormG must write itself — a Cast type name, an Extract part, a window frame= — is outside the grammar it accepts.
WritesDisabledErrorThe 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.)
UnsupportedConnectionErrorA connection object that is none of PormG's engine pools reached an execution path — an internal PormG dispatch bug; please report it. On MySQL it is also the refusal of what has no faithful SQL there: a Cast to a type with no MySQL target (interval, bytea), or a default on a binary column. (Capability limits are BackendCapabilityError; an unbound model is InvalidConfigurationError.)
DoesNotExist / MultipleObjectsReturnedget() found zero / more than one row.

Defining models

TypeRaised 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.
FieldValidationErrorA 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.
ModelDefinitionErrorA 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, a CheckConstraint with no name or a malformed condition, two constraints sharing a name, a constrained ForeignKey into an unmanaged model, or an unresolvable ForeignKey target.

FieldValidationError fires while defining a model; InvalidValueError fires while coercing a value, on a filter or a write. That is the distinction between the two.

Configuration and migrations

TypeRaised when
ConfigurationError (abstract)Umbrella for configuration failures — covers InvalidConfigurationError, MissingConfigurationError, and WritesDisabledError (listed in the Querying table above, where users meet it).
InvalidConfigurationErrorThe configuration — or the install it runs from — is 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, an attempt to overwrite a static connection, or a missing or empty upgrading/ log bundled with the install (upgrade_guide).
MissingConfigurationErrorNo 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 the four cases below.
InvalidMigrationErrorA duplicate index name in a plan, a CheckConstraint name its table already holds or a condition still naming a column the plan renames or removes, a managed model's constrained key into an unmanaged one, a plan file that does not parse, an invalid answer to an interactive makemigrations prompt or end of input at one, a renames hint that contradicts the schema, a likely rename (a same-definition pair) that makemigrations(interactive = false) was given no hint for, or a migration-engine step that cannot proceed. Having no pending plan is not one: migrate() returns outcome = :nothing_pending.
DestructiveMigrationErrorA destructive plan was applied non-interactively without destructive=true; carries the refused statements, and lossy_alters for a column change that alters existing values. Not on the using PormG surface — name it PormG.Migrations.DestructiveMigrationError.
MigrationPrecheckErrorA plan's column or constraint change would fail on existing rows (a NULL under a new NOT NULL, a value too long for a shorter max_length, duplicates under a new unique = true), or cannot run on PostgreSQL at all; refused before anything is written, whatever destructive says. Carries findings. Not on the using PormG surface — name it PormG.Migrations.MigrationPrecheckError.
PlanPreconditionErrorThe database no longer holds the schema the pending plan was generated against — a table it compared changed, appeared, or disappeared since makemigrations (#739). Checked before the migration lock and again inside it; no plan statement runs and no failed row is recorded. Carries tables. Not on the using PormG surface — name it PormG.Migrations.PlanPreconditionError.
PoolError (abstract)Umbrella for connection-pool failures — catch it to get both cases below.
PoolTimeoutError / PoolConnectErrorThe 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

UsageError above reports PormG refusing the call — almost always before a statement leaves the process (its docstring names the calls that refuse later). These report what the database refused once a statement got there. Each carries adapter ("PostgreSQL" / "SQLite" / "MySQL") and cause — the driver's own exception, kept so SQLSTATE-level detail stays reachable — instead of a msg, so read them with error_message.

TypeRaised when
DatabaseError (abstract)Umbrella for every failure raised by the database itself. catch DatabaseError covers all three below without naming a driver package.
IntegrityErrorA constraint said no — UNIQUE, FOREIGN KEY, NOT NULL, CHECK, or an exclusion constraint. The one database failure applications routinely handle rather than propagate.
OperationalErrorTransient, 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). After a dropped connection a write may already have committed, so repeat only work that is safe to run twice (Dropped connections).
StatementErrorThe 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.
TransactionErrorNot a database error: the transaction API was used in a way that cannot work — atomic(durable=true) or without_foreign_keys (PostgreSQL and SQLite; MySQL refuses it with BackendCapabilityError) nested inside an open transaction on the same database, 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 service_unavailable(error_message(e))  # the row may exist: don't resend blindly
    rethrow()
end
Classification is exact on PostgreSQL and MySQL, message-based on SQLite

LibPQ 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. On MySQL the kind is read off MySQL's error number. Treat IntegrityError as reliable on every engine; .cause is there when you need more than PormG's three kinds.

The reason is data, and the text is safe to show

Each error carries sqlstate, constraint, table, column and message — whichever the driver reports, nothing otherwise — and renders from them alone. error_message(e) never carries the database's DETAIL, so the conflict(error_message(e)) above can go to a client. See A database error is safe to show.

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)
end

Reacting to a whole category — the umbrellas make one catch enough:

try
    PormG.Configuration.load("db_2"; env = "dev")
    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)
end

Bulk Operations

bulk_insert, bulk_update, bulk_copy, allocate_primary_keys, resync_sequences

Validation

validate_value — a write's per-field checks without writing, for test doubles; see Checking a value without writing it

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) — and with_read_retry, which runs a read again after a dropped connection when you vouch it is safe to repeat

Utilities & lifecycle

upgrade_guide, register_ignore_tables!, @import_models, @models_module, @pormg_debug

PormG.import_models(path; into = Main) is the function form of @import_models (#1232), for a script or a REPL that compares several databases: it includes the models file at path (relative to pwd()), registers its models like the macro, and returns the models module.

Configuration.connection(key) returns the pool registered under key, positionally like get_settings(key), ping(key) and status(key); the keyword form connection(; key) is legacy.

`setup`, `install_ai_skills` and `import_models` are qualified-call-only

The one-off lifecycle helpers PormG.setup() (interactive project wizard), PormG.install_ai_skills() and PormG.import_models(...) are deliberately not exported — their generic names would otherwise land in every using PormG namespace. Call them qualified, exactly as shown throughout these docs.

`fetch` extends `Base.fetch`

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, Joined, 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, LPad, RPad, Substr

Math — Abs, Round, Floor, Ceil, Sqrt, Exp, Ln, Power, Mod

Type / value — Cast, Extract, ToChar, Value, Coalesce, Greatest, Least, NullIf

Full-text search (PostgreSQL) — SearchQuery, SearchVector, SearchRank, SearchHeadline — see Full-Text Search

Result-shape contract for list()

The terminal list() methods return a documented, stable shape:

CallReturns
query.list()Vector{PormGRow}
query.list(:dict)Vector{Dict{Symbol, Any}}
query.list(:json)JSON string
JSON.json(query.list())JSON string — identical to query.list(:json), and nests without double-encoding
`query> DataFrame`

Auto-Generated API Docs

The following section contains auto-generated documentation from docstrings in the source code.

What appears here

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.import_models — Method
import_models(path::AbstractString; into::Module = Main) -> Module

The function form of @import_models (#1232), for a script or a REPL session — one that compares two databases, say — where the macro's string literal and module context get in the way. Includes the models file at path (relative to pwd(), or absolute) into into, registers its models exactly as the macro does, and returns the models module:

PormG.Configuration.load("db"; env = "dev")
PormG.Configuration.load("db_archive"; env = "dev")
M = PormG.import_models("db/models.jl"; into = Module(:Live))         # both files define
A = PormG.import_models("db_archive/models.jl"; into = Module(:Archive))   # `module models`
M.Driver.objects.filter("surname" => "Senna").count(), A.Driver.objects.count()

The file must define one module; its folder must be loaded first, as with the macro. In a package, keep the macro: it resolves the path against the calling file and precompiles with the package.

source
PormG.install_ai_skills — Function
install_ai_skills(target_dir::String = pwd(); io::IO = stdout)
    -> (; installed_dir, written, stale, referenced_in, stub)

Copy PormG's pormg-usage skill bundle into <target_dir>/.github/skills/pormg-usage/, the file set an AI coding assistant (GitHub Copilot, Claude, Cursor, …) reads to learn the PormG API. Re-running it is the update mechanism: every shipped file is overwritten with the copy from the PormG version your project resolves, so run it again after each PormG bump.

The bundle is flat — SKILL.md, a router, plus the topic files it links to — and the whole directory is copied so none of its relative links dangle after install.

Claude Code registers a skill only from .claude/skills/<name>/SKILL.md, so it also writes <target_dir>/.claude/skills/pormg-usage/SKILL.md: a discovery stub holding the bundle's front-matter and a pointer to the bundle's SKILL.md, rewritten on every run. When any step of that path is a link (.claude, .claude/skills, the skill directory or the file), it writes no stub.

It writes only those two places, and reports what it found rather than acting on it:

  • written — the files it copied into the bundle directory, by name.
  • stub — the path of the Claude Code stub, or nothing when it was not written (a link is on its path, or the write failed; both are logged as a warning).
  • stale — files already in the target directory that this PormG version does not ship. Usually a topic file an older PormG installed and a later one renamed; possibly a file you added yourself. They are left in place — delete them if they came from PormG, because an assistant browsing the directory would still read their outdated content.
  • referenced_in — the project's agent-instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, .github/instructions/*.md) that mention pormg-usage. Copying the files does not make an assistant read them: when this is empty the function prints a one-line pointer to add to one of those files. It never edits them itself — many projects keep a hand-curated skills table there.

Returns nothing when the bundle could not be found or copied (the failure is logged).

source
PormG.setup — Function
setup(path::String = DB_PATH; root = nothing)

Interactively setup the connection.yml file for PormG. This will prompt for database adapter, name, and connection details. For PostgreSQL it also asks which driver opens the connections, LibPQ (the default) or Postgres. A typed answer is written as postgres_driver: into each environment block; an empty answer writes no key, and the driver is then chosen at load time (PORMG_POSTGRES_DRIVER, then LibPQ) (#1676).

root means what it means for Configuration.load: the project root path is relative to. With it, the folder is joinpath(root, path) and the AI skills (if you accept them) go into root, from any working directory. Without it, a relative path is resolved against pwd(), and the skills go into pwd() even when path is absolute; the prompt names the folder before writing to it. An absolute path with root is refused, since joinpath would discard the root (#1294).

const APP_ROOT = normpath(joinpath(@__DIR__, ".."))
PormG.setup("db"; root = APP_ROOT)   # APP_ROOT/db/connection.yml, APP_ROOT/.github/skills/…
source
PormG.upgrade_guide — Function
upgrade_guide([io::IO = stdout]; from, to = <current code>, structured = false)

Print the change-log 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 entries still marked Unreleased that 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/ log shipped with the resolved PormG install — one file per entry — 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.

Throws

  • ArgumentError when from is omitted — a mistake in the call itself.
  • InvalidConfigurationError (<: ConfigurationError <: PormGError) when the bundled upgrading/ log is missing or holds no entries — a broken install, which would otherwise print "nothing to port" and read as being up to date.

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);
source
PormG.@pormg_debug — Macro
@pormg_debug
@pormg_debug condition

Contributor-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 up
source
PormG.Kernel — Module
PormG.Kernel

Layer 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.

source
PormG.Kernel.AmbiguousFieldError — Type
AmbiguousFieldError(msg) <: FieldAccessError <: UsageError <: PormGError

A name on this query means two things at once, so it has no single meaning and PormG refuses to guess. Two cases raise it:

  • a __ path's first segment names both a declared CTE and a model field, reverse accessor, many-to-many field, JSONField or cjoin/on() join path (#492). The message prints the CTE("<name>", "<path>") spelling that selects the CTE side;
  • a filter(...) key names both a model field and a projection alias of the same name that projects something other than that column, e.g. values("points" => Sum("points")) followed by filter("points" => …) (#703). The message prints the rename that resolves it.

Deliberately its own type rather than an UnknownFieldError: the name is known twice, not unknown, and the remedy differs. A consuming app's typo handler should not also fire when a schema change makes an existing name collide.

source
PormG.Kernel.BackendCapabilityError — Type
BackendCapabilityError(msg) <: PormGError

The active backend cannot do this — a PostgreSQL-only lookup on SQLite (JSONB containment, iunaccent_*, the regex family), full-text search on SQLite (the @search lookup and the Search* functions, #31), an explicit window frame= on SQLite, bulk_copy on SQLite, with_advisory_lock(...; on_missing_lock = :error) on SQLite, a DecimalField wider than the 15 digits SQLite stores exactly (raised by makemigrations, #648), 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.

source
PormG.Kernel.ConfigurationError — Type
ConfigurationError <: PormGError  (abstract)

Umbrella for configuration failures — the connection setup, and the install PormG runs from — 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).

source
PormG.Kernel.DatabaseError — Type
DatabaseError <: PormGError  (abstract)

Umbrella for failures raised by the database itself, once a statement has reached it — as opposed to UsageError, which reports PormG refusing the call. 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. The reason the database gave is carried as data (#987), each field nothing when the driver does not report it:

FieldWhatReported by
sqlstateThe five-character SQLSTATE, e.g. "23505"both PostgreSQL drivers
constraintThe constraint that refused the rowboth PostgreSQL drivers
table, columnThe table and column the server namedboth PostgreSQL drivers
messageThe server's primary message — nothing for SQLSTATE class 22, whose message quotes the inputevery driver

The rendered text never carries a value — error_message, showerror and string alike — so it is safe to return to a client. The database's DETAIL, HINT and LINE n: excerpt quote the row, so they live only in the driver's own exception, kept in .cause for a caller that logs it to a trusted sink.

try
    M.Driver.objects.create("code" => "SEN")
catch e
    e isa IntegrityError && e.sqlstate == "23505" && return conflict(error_message(e))
    # Not `retry()`: after a dropped connection the row may already exist (#1042).
    e isa OperationalError && return service_unavailable(error_message(e))
    rethrow()
end

Connect-time failure is not here: it never reached a statement, and has been ConnectionPool.PoolConnectError under PoolError since #261.

source
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 the upgrade log's own #239 migration recipe did.

source
PormG.Kernel.DoesNotExist — Type
DoesNotExist <: PormGError

get() 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.

source
PormG.Kernel.FieldValidationError — Type
FieldValidationError(msg) <: DefinitionError <: PormGError

A 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.

source
PormG.Kernel.FilterError — Type
FilterError(msg) <: UsageError <: PormGError

An invalid filter shape — an unknown lookup, a list where one value belongs, an argument that shapes the SQL (an isnull flag) — or an operator misused on a JSON/subquery column. A bound value the filter refuses is an InvalidValueError instead (#988).

source
PormG.Kernel.IntegrityError — Type
IntegrityError(adapter, cause; sqlstate, constraint, table, column, message) <: DatabaseError <: PormGError

A 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, with the full text the server sent. On PostgreSQL this is derived from SQLSTATE class 23, so it is exact; on SQLite it comes from SQLite's own literal constraint messages. The other fields are the reason as data — see DatabaseError.

source
PormG.Kernel.InvalidConfigurationError — Type
InvalidConfigurationError(msg) <: ConfigurationError <: PormGError

The configuration — or the install it runs from — is 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), an attempt to overwrite a static connection, or a missing or empty upgrading/ log bundled with the install (upgrade_guide, #639).

source
PormG.Kernel.InvalidMigrationError — Type
InvalidMigrationError(msg) <: MigrationError <: PormGError

The migration engine refused or could not complete an operation — a duplicate index name in a plan, an invalid answer to an interactive makemigrations prompt, 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.

source
PormG.Kernel.InvalidValueError — Type
InvalidValueError(msg) <: UsageError <: PormGError
InvalidValueError(reason, kind; op, model, field, field_type, row) <: UsageError <: PormGError

A value was refused: it failed coercion/type validation on a filter, insert or update, an identifier failed the fail-closed safety check, or an interval/duration literal could not be parsed. Raised by the Models.format_*_sql coercion helpers, which every write and filter path calls (#239), and by a lookup's own checks on the value it binds — a @year bound, @family, a numeric JSON comparison (#988).

A refusal never contains the value it refused (#971): a bound value can be a password or any other secret, and an app may hand e.msg to an HTTP client. The message names where it happened — the operation, the field, the row of a bulk write — and why, and the value stays out of it.

Fields, beyond the rendered msg:

  • kind::Symbol — what was wrong, as data: :type (a value of the wrong type), :format (text that does not parse), :range (parses, but out of bounds), :nul (a NUL character, #951), :json_nul (a NUL inside a JSON document, #954), :other for everything else.
  • reason::String — the formatter's explanation, without any field or row.
  • op, model, field, field_type, row — where it happened, nothing until a funnel knows. A formatter raises with the reason alone; the funnel that knows the field and row attaches them once (with_location).
source
PormG.Kernel.LazyTraversalError — Type
LazyTraversalError(msg) <: FieldAccessError <: UsageError <: PormGError

An 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).

source
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).

source
PormG.Kernel.ModelDefinitionError — Type
ModelDefinitionError(msg) <: DefinitionError <: PormGError

A model or schema definition is invalid — more than one primary key, a duplicate related_name, a reverse accessor that shadows a field or contains __ / @, an illegal field name, a UniqueConstraint that names an unknown or many-to-many field, an unresolvable ForeignKey / ManyToManyField target, a Model(...) call given something that is not a PormGField, or a generated models file whose name cannot be its module's name (f1-models.jl, 1975.jl, Models.jl: the name without .jl must be a Julia identifier and not one the file imports, #1486).

source
PormG.Kernel.MultipleObjectsReturned — Type
MultipleObjectsReturned <: PormGError

get() 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).

source
PormG.Kernel.OperationalError — Type
OperationalError(adapter, cause; sqlstate, constraint, table, column, message) <: DatabaseError <: PormGError

The 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, for work that is safe to repeat. A connection that dropped with no error code may have dropped after the server ran the statement, so an INSERT, UPDATE or DELETE may already have committed: repeat a read freely, but repeat a write only when a second run cannot change the outcome (#1042). fetch retries a dropped connection itself only when the statement provably never ran. PormG raises this error for with_advisory_lock acquisition timeouts too: contention is a runtime condition, not misuse. So does get_or_create on PostgreSQL when its insert returns no row and the row cannot be read back (#1299): usually the row the insert conflicted with was deleted in between, and the call retried can succeed (a trigger or rule that skips the insert fails every retry). The fields are those of DatabaseError.

source
PormG.Kernel.PoolError — Type
PoolError <: PormGError

Abstract 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.

source
PormG.Kernel.PormGArrayLiteral — Type
PormGArrayLiteral(literal::String)

An ArrayField value on its way to PostgreSQL — the wrapper the field's formatter returns (#28). literal is the array in PostgreSQL's own text form ({1,2}, {"a b",NULL}), already validated element by element and printed the way PostgreSQL's array_out prints it.

PormG binds this text as ONE parameter and lets the column type it, rather than handing a Julia vector to the driver: LibPQ and Postgres.jl each render a vector differently (LibPQ writes nothing and Inf as the words), and a bare vector already means "a list of values" to the parameter collectors, which expand it for @in. The wrapper is not an AbstractArray, so no collector can mistake it for one — the PormGBytes reasoning.

Layer 1 for the same reason as PormGBytes: Models produces it and QueryBuilder consumes it.

source
PormG.Kernel.PormGBackend — Type
PormGBackend <: PormGAbstractType

The backend/dialect marker — the dispatch key for SQL rendering and driver selection.

Its subtypes, PormGPostgres, PormGSQLite and PormGMySQL, are what every dialect method dispatches on, so a function that renders SQL differently per backend is written as one method per engine rather than as a runtime branch. The values you actually hold are the concrete connection pools: PostgresConnectionPool <: PormGPostgres, SQLiteConnectionPool <: PormGSQLite and MySQLConnectionPool <: PormGMySQL.

PormGMySQL is MySQL 8.4 or newer (#1130): reads, writes, migrations and inspectdb. What it lacks is refused before any SQL is sent — see the MySQL Guide.

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.

source
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}, and bytea'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.

source
PormG.Kernel.PormGError — Type
PormGError <: Exception

Root 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)
end

Use 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 (UsageError, FieldAccessError, PoolError, DatabaseError, ConfigurationError, MigrationError, DefinitionError) to group a family. UsageError is the one to catch for "PormG refused what I passed in": it is narrower than this root, and it keeps matching when a refusal moves between its members.

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.

source
PormG.Kernel.PormGField — Type
PormGField <: PormGAbstractType

Base 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). The way back is not the field's job: values read back are decoded by the representation table (value_parser), per canonical kind and backend.

See also PormG Field Types Reference.

source
PormG.Kernel.PormGModel — Type
PormGModel <: PormGAbstractType

Base 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.

source
PormG.Kernel.ProtectedError — Type
ProtectedError(msg) <: PormGError

A 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.

source
PormG.Kernel.QueryBuildError — Type
QueryBuildError(msg) <: UsageError <: PormGError

Structural/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.

source
PormG.Kernel.SQLObject — Type
SQLObject <: PormGAbstractType

Base 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.

source
PormG.Kernel.SQLObjectHandler — Type
SQLObjectHandler <: SQLObject

Base 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).

A query is a Tables.jl table

A query is a Tables.jl source (#1344), so any Tables.jl sink reads it: DataFrame(query) after using DataFrames, CSV.write(io, query), Tables.rowtable(query). Each read executes the query once.

The columns carry the same values list() returns: a temporal column or expression alias holds ZonedDateTime / Date / Time / CompoundPeriod values on every engine, not the driver's raw text (#582). To keep the engine's own text on purpose, project it as text: ToChar(...) or Cast(F(col), "TEXT").

using DataFrames
query = M.Result.objects.filter("raceid__year" => 2020).
    values("driverid__forename", "constructorid__name", "laps")
df = query |> DataFrame

See also SQLObject (the state it wraps).

source
PormG.Kernel.StatementError — Type
StatementError(adapter, cause; sqlstate, constraint, table, column, message) <: DatabaseError <: PormGError

A 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 and the fields are those of DatabaseError; 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 get_or_create on SQLite finding no row after its insert reported success (#1299) — and claiming the server refused something it never saw would send a reader hunting for a SQL bug that does not exist.

source
PormG.Kernel.TransactionError — Type
TransactionError(msg) <: PormGError

The transaction API was used in a way that cannot work — atomic(durable=true) or without_foreign_keys nested inside an open transaction on the same database, 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. Every case is 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).

source
PormG.Kernel.UnknownFieldError — Type
UnknownFieldError(msg) <: FieldAccessError <: UsageError <: PormGError

A field, alias, column, or __ lookup path does not exist on the model or the projected row.

source
PormG.Kernel.UnsafeMutationError — Type
UnsafeMutationError(msg) <: UsageError <: PormGError

An UPDATE or DELETE was requested without a filter (or in another unsafe shape) and refused.

source
PormG.Kernel.UnsupportedConnectionError — Type
UnsupportedConnectionError(msg) <: PormGError

Two cases, each named by its message. A connection object that is none of PormG's engine pools reached an execution path — a PormG internal dispatch bug; the message asks the user to report it. Or MySQL has no faithful SQL for what was asked: a Cast to a type with no MySQL target that holds its meaning (interval, bytea, a type of your own), or a default on a binary column. 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.

source
PormG.Kernel.UsageError — Type
UsageError <: PormGError  (abstract)

Umbrella for "PormG refused the call for what it asked" — catch it to answer a bad request without naming the concrete refusal. Its members:

What it promises: PormG refused the call for what it asked, not because the database failed, and the remedy is to change the call — retrying it unchanged is not one. Catching the umbrella keeps working when a refusal moves between its members, which has happened on more than one train (#576, #971, #988).

try
    M.Result.objects.filter("points__@gte" => user_supplied).list()
catch e
    e isa UsageError || rethrow()
    return bad_request(error_message(e))
end

It does not promise that nothing reached the database. Most refusals are raised while the query is built, but a few come after statements of the same call ran:

  • a bulk write validates each row as it goes, so a later row is refused after earlier chunks were sent (and, with allocate_pk, after ids were drawn);
  • delete() probes the related tables, and may already have run its cascade UPDATEs and DELETEs;
  • get_or_create reports a missing UNIQUE constraint on its lookup fields when the database rejects the ON CONFLICT.

Without a transaction of the caller's, the call's own transaction rolls those statements back. Inside run_in_transaction they are the caller's to roll back: a block that catches a bulk's or a delete()'s UsageError and returns normally commits what they ran (after a get_or_create refusal, PostgreSQL has already aborted the transaction), so rethrow it, or catch it outside the block.

Deliberately not members, because a bad request is the wrong answer to them: BackendCapabilityError (the remedy is the backend), ProtectedError (the data forbids the delete), UnsupportedConnectionError (a PormG bug), and the DatabaseError family (the database itself said no).

source
PormG.Kernel.WritesDisabledError — Type
WritesDisabledError(msg) <: ConfigurationError <: PormGError

The 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.

source
PormG.Kernel.error_message — Method
error_message(e::PormGError) -> String

The 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)
end

Defined 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.

source
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"])
source
PormG.Functions — Module
PormG.Functions

The 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, LPad, RPad, Substr

Math — Abs, Round, Floor, Ceil, Sqrt, Exp, Ln, Power, Mod

Type / value — Cast, Extract, ToChar, Value, Coalesce, Greatest, Least, NullIf

Full-text search (PostgreSQL) — SearchQuery, SearchVector, SearchRank, SearchHeadline — see Full-Text Search

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.

source
PormG.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.

source
PormG.QueryBuilder.ObjectHandler — Type
ObjectHandler <: SQLObjectHandler

The 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.

source
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.

source
PormG.QueryBuilder.WindowSpec — Type
WindowSpec <: SQLType

The 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 to DESC at build time.
  • frame::Union{String,Nothing} — an explicit frame clause, or nothing for the SQL default (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). PostgreSQL only. Held in the grammar WindowOver documents: WindowOver stores its own rebuilt spelling, and a frame assigned here directly is parsed when the query is built, raising InvalidValueError if it is outside that grammar.

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.

source
Base.first — Method
first(objct::SQLObjectHandler; show_query::Symbol = :execute)

Return the first PormGRow matching the current query, or nothing if no records match.

A slice the caller set is honored: first takes the first row of the slice, so q.limit(0).first() is nothing and q.offset(10).first() is the eleventh row (#1053).

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 handler
source
Base.first — Method
first(objct::SQLObjectHandler, T::Type; show_query = :execute) -> Union{Nothing, T}

first as a typed read (#1352): the first row built into T, or nothing. The projection and conversion rules are list(T)'s.

source
Base.get — Method
get(objct::SQLObjectHandler, filters...; show_query=:execute) -> PormGRow

Return 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.

A slice the caller set is honored: get looks for its one row inside the slice, so q.limit(0).get() raises DoesNotExist (#1053). Inline filters join the WHERE clause before the slice applies, the same as a filter() call after limit().

source
Base.get — Method
get(objct::SQLObjectHandler, T::Type, filters...; show_query = :execute) -> T

get as a typed read (#1352): exactly one row, built into T. Raises DoesNotExist and MultipleObjectsReturned as get does.

row = M.Driver.objects.get(DriverRow, "driverref" => "senna")
source
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.

On a query with limit() or offset() set, last raises QueryBuildError, as Django's does: reversing the ordering changes which rows the slice holds (#1053).

source
Base.last — Method
last(objct::SQLObjectHandler, T::Type; show_query = :execute) -> Union{Nothing, T}

last as a typed read (#1352).

source
PormG.QueryBuilder.Abs — Method
Abs(column)

Returns the absolute value of a number. Over an integer it is an integer of the same type on both engines; over any other number PostgreSQL computes a numeric (#1147), and the value reads back as the operand's type on both engines: a Float64 over a float, a Decimal over a decimal (#1121).

source
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.

The mean reads back as a Float64 over integers or floats and as a Decimal over a DecimalField, on both engines (#1121), as Django's Avg types it. PostgreSQL computes avg(integer) as a numeric, which PormG reads as the Float64 it means; SQLite computes a decimal mean as a double, which PormG reads as a Decimal to 15 significant digits.

An Avg over a BooleanField is refused with a QueryBuildError, as Sum is (#953): the share of true rows is Avg(When("is_active" => true, then = 1, otherwise = 0)).

The mean of a duration is a duration. An Avg over a date, a timestamp, a time or a UUID is refused with a QueryBuildError on every engine (#1387): none is a number. Average the number you mean, such as Avg("date__@year").

See also Filters and Aggregates.

source
PormG.QueryBuilder.CTE — Method
CTE(name::AbstractString, path::AbstractString; desc::Bool = false) -> CTEReference

Reference a column of a CTE declared with .with(...) as an object rather than as the ordinary "<name>__<path>" string. Both spellings mean the same column and render identical SQL; the object is the disambiguator (#444, #492).

A CTE name that collides with nothing needs no object — values("ev__sku") is the idiomatic spelling. Write the handle when the CTE's name also names something on the model, because then the shared __ path has two readings and PormG refuses to choose:

driver_totals = M.Result.objects
driver_totals.values("driverid", "n" => Count("resultid"))

q = M.Result.objects
q.with("driverid" => driver_totals)       # "driverid" is ALSO a ForeignKey of Result
q.values("points", "driverid__surname")   # → AmbiguousFieldError: the FK, or the CTE?
q.values("points", "n" => CTE("driverid", "n"))   # the CTE's column, explicitly

It is also required on the right of a filter pair, where a bare string is a value rather than a column: filter("surname" => CTE("d91", "surname")) correlates, while filter("surname" => "d91__surname") compares surname against that literal text — quietly matching nothing on a text column, and raising FilterError on a numeric one.

A handle sits on either side of a comparison operator (==, !=, <, <=, >, >=), as F(...) does: filter(CTE("driverid", "n") > 5) and filter(F("points") == CTE("d91", "points")) (#1529). It takes + - * / on either side as F(...) does too — CTE("best", "best") - F("points"), 2 * CTE("driverid", "n"), CTE("s", "first") + Day(7) — and a date, timestamp or duration column is typed as one, so its arithmetic is date arithmetic on every engine (#1539).

The 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.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, byte-identical to what values("parent__sku") produces.

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. Both take the string's - prefix too, so it is a convenience rather than the only way to order descending. 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.

source
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: a Vector of When branches, or a single bare When.
  • default: the ELSE branch. Defaults to the string "NULL", which is emitted as the SQL literal NULL — it is not a bound parameter, so pass a Julia value (0, "") when you want a real default. missing is emitted as NULL too, and a column expression (F("grid")) renders as that column.
  • output_field: the result type. Accepts a PormGField instance (e.g. CharField(), whose .type is used; a DecimalField casts to its numeric(max_digits, decimal_places), #1221) or a raw SQL type string. Renders as a ::type cast on PostgreSQL and a CAST(...) on SQLite — date(...) for a date, and a time type raises BackendCapabilityError there (#822). In a CTE body it is also the column's type; without it the type is inferred from the branches, and branches that do not agree raise QueryBuildError — see How a CTE Column Is Typed. A text, integer or scaled numeric(p, s) output_field refuses, branch by branch, the operands Cast refuses (#1028, #1040, #1217): round a float branch first (then = Round("points", 1) at numeric(10,1)).

Usable anywhere a column expression is — in values(), nested inside Sum, as a filter right-hand side, and in .update().

Without output_field, a projected Case reads back as the type its branch values and default agree on, on every engine, as Coalesce does. Over a DurationField, Case([When("grid" => 1, then = F("fastestlaptime"))]) is a Dates.CompoundPeriod on SQLite as on PostgreSQL, not the stored interval text (#1262). Values of different types read back as the driver delivers them.

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.

source
PormG.QueryBuilder.Cast — Method
Cast(expression, type)

Casts a column or expression to a SQL type — PostgreSQL (x)::type, SQLite CAST(x AS type). SQLite has no time types: a date target renders date(x) there, and timestamp, timestamptz, time and interval raise BackendCapabilityError (#822).

type is preferably a field object (Cast("grid", BigIntegerField())), which renders in each engine's own spelling. A DecimalField casts to its own precision and scale (#1221): Cast(x, DecimalField(max_digits = 10, decimal_places = 1)) is Cast(x, "numeric(10,1)"), and DecimalField() is numeric(10,2). A CharField given a max_length casts to that length (#1252): Cast(x, CharField(max_length = 3)) is Cast(x, "varchar(3)"), while CharField() declares none and casts to unbounded text. A SlugField or URLField casts to the length its column has (#1438), varchar(50) / varchar(200) by default; an EmailField declares none. An ArrayField casts to its element's type with [] (ArrayField(CharField(max_length = 3)) is "varchar(3)[]"), on PostgreSQL only: SQLite and MySQL raise BackendCapabilityError. A string is accepted when it is a single type name ("integer", "bigint", "text", "timestamptz"), one of the multi-word names "double precision", "character varying", "bit varying", "timestamp with time zone" (and without, and the time forms), optionally followed by a size (n) or (n, m) — "numeric(10,2)", "varchar(20)" — and, on PostgreSQL only, array brackets ("integer[]"). Anything else raises InvalidValueError when the expression is built, on both engines: a type name is a keyword in the SQL, so it cannot be a bind parameter, and PormG only writes a spelling it has parsed. The same rule applies to every output_field= string.

The expression may be a column path, an F(...) expression, another function or a Subquery(...) — Cast(Subquery(s), "date") casts the one value the subquery returns per row.

A cast is checked against what it means on each engine (#1147): the engine that would compute a different value raises QueryBuildError when the query is built, and the others build it.

  • to text (CharField(), "text", …): a float, a decimal, a timestamp, an interval or a JSON document builds everywhere, each engine writing its own text ('25' or '25.0'). A boolean means 'true'/'false', and SQLite, which writes '1', refuses it (#1028);
  • to an integer, a float, a decimal or a numeric function means the nearest integer, and SQLite, whose cast truncates, refuses it (#1028). Say how to round to build it everywhere: Cast(Round(x), IntegerField()), Floor(x) or Ceil(x). A boolean casts to 1/0 and passes;
  • to numeric(p, s) (or numeric(p), scale 0), an operand with more than s digits after the point (a float column, a float literal with more places (#1050), text, a numeric function, a decimal with more places) means the value rounded to the scale, and SQLite, which keeps every digit, refuses it (#1040). Round it first, Cast(Round(x, 2), "numeric(10,2)"), or cast to an unscaled "numeric". A DecimalField target is its numeric(max_digits, decimal_places);
  • to a single-precision float ("real", "float4", "float(p)" for p ≤ 24), any value means the value rounded to a 4-byte float, a Float32, and SQLite, which keeps the 8-byte double, refuses it (#1215). Cast to "float8" or FloatField(), a Float64 on every engine;
  • to text of a declared length ("varchar(n)", "char(n)", "char", which is char(1), PostgreSQL's "bpchar(n)" and "nchar(n)", CharField(max_length = n), SlugField, URLField), any value means the text cut to n characters, and SQLite, which enforces no length, refuses it (#1252). Cast to "text" or CharField() and cut the fetched value in Julia.
using PormG.Functions: Cast, Round
using PormG.Models: IntegerField

M.Result.objects.values("resultid", "points_int" => Cast(Round("points"), IntegerField()))
M.Result.objects.values("resultid", "points_num" => Cast("points", "numeric"))

See also Functions and Dates and Rounding.

source
PormG.QueryBuilder.Ceil — Method
Ceil(column)

Returns the smallest integer greater than or equal to a number. Over an integer it is that integer, of the same type, on both engines; over any other number PostgreSQL computes a numeric (#1147), and the value reads back as the operand's type on both engines: a Float64 over a float, a Decimal over a decimal (#1121).

source
PormG.QueryBuilder.Coalesce — Method
Coalesce(args...; output_field=nothing)

Returns the first non-null value in the list of arguments. It takes two or more arguments: fewer raise QueryBuildError when the expression is built (#859), since one argument is the argument itself.

A string argument is a column path. A number, a Bool, or a Date/DateTime/ZonedDateTime/Time is a literal that is bound as a parameter (Coalesce("points", 0) means Coalesce("points", Value(0))). Wrap a string literal in Value. Any other value raises QueryBuildError when the expression is built (#705).

The value reads back as the column's own Julia type on both engines when every argument is of one type — Coalesce("date", "fp1_date") is a Date, not SQLite's stored text. When the argument types differ, the value comes back as the engine delivers it (#824): on SQLite, the stored value of the argument that won.

output_field casts the result to the type it names, on both engines (#852): Coalesce("number", 0; output_field = "integer") renders (…)::integer on PostgreSQL and CAST(… AS INTEGER) on SQLite, so the value, a filter on it, and a CTE column typed by it all agree. On SQLite a date renders date(…), and the other temporal types and arrays raise BackendCapabilityError, as for Cast. A text or integer output_field refuses the operands Cast refuses (#1028): over a float, round first (Floor("points")). So does a scaled numeric(p, s) one (#1040).

source
PormG.QueryBuilder.Concat — Method
Concat(expressions; output_field=nothing)

Concatenates multiple strings or columns.

A NULL operand is skipped, read as an empty string, on both engines, so the result is never NULL: Concat(Value("#"), "number", Value(" "), "surname") is "# Senna" for a driver with no number. This follows Django's Concat. PostgreSQL renders CONCAT(…), which skips a NULL itself, and SQLite renders COALESCE(operand, '') || …. To get NULL when a column is NULL, test it explicitly: Case(When("number__@isnull" => false, then = Concat(…))) — a Case with no match is NULL.

Text, integer, date, time and uuid operands read the same on both engines. Any other operand (a boolean, a float, a decimal, a timestamp, an interval or a whole JSON document) raises QueryBuildError on every engine, because the query does not say which text it means (#1027, #1028, #1147): a literal (true, 1.5, a Decimal) when the expression is built, a column or an expression of those types when the query is. Say it: Cast(x, "text"), which writes each engine's own text, a Case for a boolean, ToChar(x, "YYYY-MM-DD HH:MI:SS") for a timestamp, or format the fetched value in Julia.

The result is text on both engines, so output_field, when given, must be a text type (CharField(), TextField(), "text", "varchar(20)"). Any other type raises InvalidValueError when the expression is built. Concat renders no cast, so a number declared there would be a type the SQL never applies. To get a number, cast the result explicitly: Cast(Concat(…), "integer").

source
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: If true, counts only distinct values of x.

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 |> DataFrame
source
PormG.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.

source
PormG.QueryBuilder.Exists — Method
Exists(query::SQLObjectHandler) -> ExistsObject

Wrap 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))

A projected Exists reads back as a Bool on both engines; SQLite evaluates it to 0/1, which PormG reads back as a Bool (#965).

A limit() or offset() on the subquery is honored: Exists(sub.limit(0)) is always false, and Exists(sub.offset(2)) asks whether sub has a third row (#1053). Its order_by() is dropped, because an EXISTS answer cannot depend on it.

See also Subquery for the scalar (single-value) form and Subqueries and CTEs.

source
PormG.QueryBuilder.Exp — Method
Exp(column)

Returns the exponential value (e^x) of a number.

It reads back as a Decimal over a DecimalField and as a Float64 over an integer or a float, on both engines (#1121), as Django's Exp types it. PostgreSQL computes it as a numeric.

source
PormG.QueryBuilder.Extract — Method
Extract(column, part)

Extracts a component ("year", "month", "dow", …) from a date/time column.

part is case-insensitive and must be a PostgreSQL EXTRACT field: CENTURY, DAY, DECADE, DOW, DOY, EPOCH, HOUR, ISODOW, ISOYEAR, JULIAN, MICROSECONDS, MILLENNIUM, MILLISECONDS, MINUTE, MONTH, QUARTER, SECOND, TIMEZONE, TIMEZONE_HOUR, TIMEZONE_MINUTE, WEEK, YEAR. Anything else — PostgreSQL's synonyms such as "years" or "hr" included — raises InvalidValueError when the expression is built, on every engine. SQLite runs YEAR MONTH DAY HOUR MINUTE SECOND DOW DOY WEEK ISOYEAR ISODOW, numbered as PostgreSQL numbers them, and raises BackendCapabilityError for the rest. MySQL runs those and QUARTER, EPOCH, MILLISECONDS and MICROSECONDS with PostgreSQL's meaning (#1130), and raises BackendCapabilityError for the rest and for the EPOCH of a time. A date part of a time expression raises QueryBuildError there, since MySQL reads a time's date as today's.

The part must be one the column holds, checked against the field the column is declared as when the query is built, on both engines (#1070). It raises QueryBuildError otherwise:

  • the time-of-day parts (HOUR, MINUTE, SECOND, MILLISECONDS, MICROSECONDS) read a DateTimeField or a TimeField;
  • the TIMEZONE parts read a DateTimeField with a time zone (the default TIMESTAMPTZ);
  • EPOCH reads any of those, a DateField or a DurationField;
  • every other part reads a DateField or a DateTimeField.

The "col__@hour" transforms are this function, so they follow the same rule. An operand PormG cannot name a field for — an expression, a subquery, an untyped CTE column — is checked by its type: a duration expression (a timestamp difference, interval arithmetic, a Sum or Avg of a duration, a time minus a time on PostgreSQL) reads only EPOCH, as a DurationField does, and raises QueryBuildError under any other part on every engine (#1388). Anything else passes unchecked.

A filter on the result is held to the part's range: HOUR 0–23, MINUTE and SECOND 0–59, MONTH 1–12, DAY 1–31, QUARTER 1–4, WEEK 1–53, ISODOW 1–7, DOW 0–6, DOY 1–366. A value outside it raises InvalidValueError rather than matching nothing.

EPOCH, MILLISECONDS, MICROSECONDS and JULIAN are decimals on every engine that has them, read back as a Decimal; PostgreSQL renders them EXTRACT(… FROM x)::numeric (#1457, #1552). MICROSECONDS, and JULIAN of a date, are whole; the others carry a fraction, so Concat over one asks for a cast to text (#1027).

To change the result type, wrap it in Cast — e.g. on PostgreSQL, Cast(Extract("date", "epoch"), "bigint").

source
PormG.QueryBuilder.F — Method
F(field_name::AbstractString) -> FExpression

Reference 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. A period is also an interval value (#1553): * scales one by a number (F("lap") * Second(90), either way round), / divides one written first (Minute(3) / F("lap")), and - takes one beside an interval on either side (F("time") - Second(1), Minute(4) - F("time")), as + does (F("time") + Second(1)). A number divided by a period is refused.

# 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).

Every operator builds a new expression and leaves its operands untouched, so one handle can be bound to a name and reused across as many predicates as you like:

pts = F("points")
M.Result.objects.filter(pts > 10, pts < 25)   # two independent predicates on the same column

See also Field Expressions.

source
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.

source
PormG.QueryBuilder.Floor — Method
Floor(column)

Returns the largest integer less than or equal to a number. Over an integer it is that integer, of the same type, on both engines; over any other number PostgreSQL computes a numeric (#1147), and the value reads back as the operand's type on both engines: a Float64 over a float, a Decimal over a decimal (#1121).

source
PormG.QueryBuilder.Greatest — Method
Greatest(args...; output_field=nothing)

Returns the greatest value in the list of arguments. It takes two or more arguments: fewer raise QueryBuildError when the expression is built (#859), since one argument is the argument itself.

A string argument is a column path. A number, a Bool, or a Date/DateTime/ZonedDateTime/Time is a literal that is bound as a parameter (Greatest("points", 0) means Greatest("points", Value(0))). Wrap a string literal in Value. Any other value raises QueryBuildError when the expression is built (#705).

The value reads back as the column's own Julia type on both engines when every argument is of one type — Greatest("date", "fp1_date") is a Date, not SQLite's stored text. When the argument types differ, the value comes back as the engine delivers it (#824): on SQLite, the stored value of the argument that won.

output_field casts the result to the type it names, on both engines (#852): Greatest("grid", 1; output_field = "integer") renders (…)::integer on PostgreSQL and CAST(… AS INTEGER) on SQLite, so the value, a filter on it, and a CTE column typed by it all agree. On SQLite a date renders date(…), and the other temporal types and arrays raise BackendCapabilityError, as for Cast. A text or integer output_field refuses the operands Cast refuses (#1028): over a float, round first (Floor("points")). So does a scaled numeric(p, s) one (#1040).

Over an uncast JSON key (Greatest("metadata__wins", "constructorid")) it raises QueryBuildError on every engine (#1497), as Max does: the key's value has no declared type, so the query does not say whether it compares numbers or text. Cast the key, Cast("metadata__wins", "numeric"); an output_field casts the result after the comparison, so it does not say it.

source
PormG.QueryBuilder.Joined — Method
Joined(alias::AbstractString, path::AbstractString; desc::Bool = false) -> JoinedReference

Reference a column of the joined copy declared by .cjoin_on(...). The joined copy's columns live in their own namespace, so an alias may share a name with a model field or with a CTE and neither shadows the other (#481):

q = M.Result.objects
q.cjoin_on(M.Driver, alias = "d", on = [Joined("d", "driverid") == F("driverid")])
q.values("points", "who" => Joined("d", "surname"))
q.filter(Joined("d", "nationality") => "Brazilian")

Inside a cjoin_on on list the two sides of the join are named by how you write the reference:

You writeResolves to
F("col") (bare)the base table — the query's own model
Joined("d", "col")the joined copy declared under alias = "d"

A reference may name another cjoin_on's alias, which is how a join correlates against a third table, in either declaration order: the alias it names is emitted first (#982). Operator suffixes work in filter(...), so a comparison against a literal on the joined side is an ordinary pair — filter(Joined("d", "points__@gte") => 3).

desc = true is meaningful in order_by(...) and in a window's order_by; anywhere else it raises a QueryBuildError. An unaliased projection is named by joining the two with a double underscore, so values(Joined("d", "surname")) emits the output column d__surname.

This replaces the F("d.surname") dotted-string spelling, which was removed in the same change: it resolved fail-open (a typo in the alias reported an unknown field named "typo.col"), it could not carry an operator suffix, and the bare string form could not be projected — values("d.surname") raised, because the dotted branch lived on the filter resolver only. (Wrapped in F(...) it did project, since F routes through that resolver.)

See also Custom Joins.

source
PormG.QueryBuilder.LPad — Function
LPad(column, len, fill = " ")

Pads a string on the left with fill until it is len characters long — LPad(Cast("number", "text"), 3, "0") turns 44 into "044". A string longer than len is cut to its first len characters, a fill of several characters repeats and is cut where the length is reached (LPad("code", 6, "xy") over "HAM" is "xyxHAM"), an empty fill pads nothing, and a NULL string is NULL. The same on PostgreSQL (LPAD), on SQLite, which has no LPAD and calls a function PormG registers on every connection it opens (pormg_lpad), and on MySQL (LPAD, an empty literal fill rendered as the cut, since MySQL's pads to ""; a fill column empty on a row gives "" there, #1130).

column is a column path or an expression, and it must be text: PostgreSQL has no lpad over a number, a date, a time, a boolean, a uuid or a JSON document, so such a column raises QueryBuildError when the query is built, on both engines. Convert it to text first — an integer, a date, a time or a uuid with Cast (Cast(x, "text")), a timestamp with ToChar. fill is text too: a string there is a literal, and a number, a date or a boolean (a literal or a column) raises QueryBuildError. A len below 0 or above 268435454 (PostgreSQL's limit in a UTF-8 database) raises InvalidValueError.

Zero-filling an integer column into a text column, in one statement:

M.Driver.objects.
    filter("number__@isnull" => false).
    update("code" => LPad(Cast("number", "text"), 3, "0"))
source
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 — a column path, an expression, or a Subquery(...) projecting one value per row. Required — passing nothing raises QueryBuildError.
  • offset: how many rows back. Must be non-negative; negatives raise QueryBuildError (use Lead to look forward).
  • default: value returned at the window edge where no previous row exists. Omit it and those rows come back missing/NULL.
  • over: the WindowOver spec. order_by is what makes "earlier" meaningful.

offset and a plain-value default are bound as query parameters, not interpolated. A column expression as default — default = F("grid") — renders as that column instead.

See also Lead, Window Functions.

source
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.

The default frame makes this return the current row

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 and MySQL
)

frame renders on PostgreSQL and MySQL (BackendCapabilityError on SQLite; on MySQL a GROUPS frame or an EXCLUDE clause raises it too). On SQLite, drop the order_by so the whole partition is one frame, or compute the value another way.

See also FirstValue, Window Functions.

source
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.

source
PormG.QueryBuilder.Least — Method
Least(args...; output_field=nothing)

Returns the least value in the list of arguments. It takes two or more arguments: fewer raise QueryBuildError when the expression is built (#859), since one argument is the argument itself.

A string argument is a column path. A number, a Bool, or a Date/DateTime/ZonedDateTime/Time is a literal that is bound as a parameter (Least("points", 25) means Least("points", Value(25))). Wrap a string literal in Value. Any other value raises QueryBuildError when the expression is built (#705).

The value reads back as the column's own Julia type on both engines when every argument is of one type — Least("date", "fp1_date") is a Date, not SQLite's stored text. When the argument types differ, the value comes back as the engine delivers it (#824): on SQLite, the stored value of the argument that won.

output_field casts the result to the type it names, on both engines (#852): Least("grid", 25; output_field = "integer") renders (…)::integer on PostgreSQL and CAST(… AS INTEGER) on SQLite, so the value, a filter on it, and a CTE column typed by it all agree. On SQLite a date renders date(…), and the other temporal types and arrays raise BackendCapabilityError, as for Cast. A text or integer output_field refuses the operands Cast refuses (#1028): over a float, round first (Floor("points")). So does a scaled numeric(p, s) one (#1040).

Over an uncast JSON key (Least("metadata__wins", "constructorid")) it raises QueryBuildError on every engine (#1497), as Max does: the key's value has no declared type, so the query does not say whether it compares numbers or text. Cast the key, Cast("metadata__wins", "numeric"); an output_field casts the result after the comparison, so it does not say it.

source
PormG.QueryBuilder.Ln — Method
Ln(column)

Returns the natural logarithm of a number.

It reads back as a Decimal over a DecimalField and as a Float64 over an integer or a float, on both engines (#1121), as Django's Ln types it. PostgreSQL computes it as a numeric.

source
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 reads back as the same Julia type the column itself does, on both engines: a MAX over a DateField is a Date, over a DurationField a Dates.CompoundPeriod (#800). MAX returns one of the column's own values, so the column's read-side parser applies to it. This does not extend to a computed aggregate: Sum and Avg come back as the engine delivers them.

Over a BooleanField it answers "is any row true?" and reads back as a Bool on both engines. It renders BOOL_OR(x) on PostgreSQL, which has no max(boolean), and MAX(x) over SQLite's stored 0/1 (#953).

Over an uncast JSON key (Max("metadata__wins")) it raises QueryBuildError on every engine (#1491): the key's value has no declared type, so the query does not say whether it orders numbers or text. Write Max(Cast("metadata__wins", "numeric")) or Max(Cast("metadata__wins", "text")).

See also Min, Filters and Aggregates.

source
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 reads back as the column's own Julia type on both engines (#800). Over a BooleanField it answers "are all rows true?": BOOL_AND(x) on PostgreSQL, MIN(x) on SQLite (#953). Over an uncast JSON key it raises QueryBuildError, as Max does (#1491).

See also Filters and Aggregates.

source
PormG.QueryBuilder.Mod — Method
Mod(dividend, divisor)

Returns the remainder (modulo) of a division.

It reads back as a Decimal when an operand is a decimal and as a Float64 otherwise, on both engines (#1121), as Django's Mod types it: Mod("grid", 2) is 1.0 for an odd grid, not 1.

A string argument is a column path. A number, a Bool, or a Date/DateTime/ZonedDateTime/Time is a literal that is bound as a parameter (Mod("points", 2) means Mod("points", Value(2))). Wrap a string literal in Value. Any other value raises QueryBuildError when the expression is built (#705).

source
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.

source
PormG.QueryBuilder.NullIf — Method
NullIf(field1, field2)

Returns NULL if field1 equals field2, otherwise returns field1.

A string argument is a column path. A number, a Bool, or a Date/DateTime/ZonedDateTime/Time is a literal that is bound as a parameter (NullIf("points", 0) means NullIf("points", Value(0))). Wrap a string literal in Value. Any other value raises QueryBuildError when the expression is built (#705).

NullIf("code", "") compares two columns; write NullIf("code", Value("")) for the empty string.

The value is field1's, so it reads back as field1's own Julia type on both engines: NullIf("date", "fp1_date") is a Date, not SQLite's stored text (#824).

Over an uncast JSON key in either operand (NullIf("metadata__wins", 0)) it raises QueryBuildError on every engine (#1510): a key's value has no declared type, so the query does not say whether the comparison is between numbers or text. Cast the key: NullIf(Cast("metadata__wins", "numeric"), 0).

source
PormG.QueryBuilder.OuterRef — Method
OuterRef(field_name::AbstractString) -> OuterRefObject

Reference 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.

It binds to the immediately enclosing query, as Django's does. A subquery nested inside another subquery — in a filter, a projection, a When or a join condition — correlates to the subquery around it, never further out (#938). Reaching two levels up (Django's OuterRef(OuterRef(…))) is not supported, and raises QueryBuildError. That holds when both enclosing levels have the column, without a warning (#952): to use the outer value further in, correlate the middle query on it, and the inner reference carries it down.

Used outside an Exists/Subquery build there is no outer query to bind to, so an OuterRef there raises QueryBuildError — wrapped in a function or not.

An outer column may be wrapped in a scalar function or a window column inside the correlated query — Lower(OuterRef("surname")), Cast(OuterRef("driverid"), "text"), Lag(OuterRef("driverid"), over = …) — and resolves against the outer row exactly as the bare reference does.

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.

source
PormG.QueryBuilder.Power — Method
Power(base, exponent)

Returns base raised to the power of exponent.

It reads back as a Decimal when an operand is a decimal and as a Float64 otherwise, on both engines (#1121), as Django's Power types it: Power("grid", 2) is 4.0 for a grid of 2.

A string argument is a column path. A number, a Bool, or a Date/DateTime/ZonedDateTime/Time is a literal that is bound as a parameter (Power("points", 2) means Power("points", Value(2))). Wrap a string literal in Value. Any other value raises QueryBuildError when the expression is built (#705).

source
PormG.QueryBuilder.Q — Method
Q(x...)

Create a QObject with the given filters.

Arguments

  • x...: key-value pairs, Qor(x...), or Q(x...) objects.

Example

a = object("tb_user")
a.filter(Q("name" => "John", Qor("age" => 18, "age" => 19)))
source
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 a SQLTypeQ or SQLTypeQor object, or a Pair object.

Example

a = object("tb_user")
a.filter(Qor("name" => "John", Q("age__gte" => 18, "age__lte" => 19)))
source
PormG.QueryBuilder.RPad — Function
RPad(column, len, fill = " ")

Pads a string on the right with fill until it is len characters long — RPad("code", 5, ".") turns "HAM" into "HAM..". Like LPad, a longer string is cut to its first len characters, a fill of several characters repeats and is cut, an empty fill pads nothing, and a NULL string is NULL, on PostgreSQL (RPAD) and SQLite (pormg_rpad) alike.

column and fill must be text, as for LPad: anything else raises QueryBuildError when the query is built, and a len below 0 or above 268435454 raises InvalidValueError.

source
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.

source
PormG.QueryBuilder.Replace — Method
Replace(column, find, replace)

Replaces all occurrences of find with replace in the string.

column is a column path. find and replace are text: a string there is a literal, and a number raises QueryBuildError naming its string spelling (#705).

source
PormG.QueryBuilder.Round — Function
Round(column, precision=0)

Rounds a number to precision decimal places, half away from zero.

Each engine rounds with its own ROUND, as Django's Round does: PostgreSQL renders ROUND(x::numeric, d), which rounds the value's exact decimal form (a DecimalField keeps its Decimal), and SQLite ROUND(x, d), which rounds the stored double. The two agree except at a decimal tie whose double sits just below it: Round(2.675, 2) is 2.68 on PostgreSQL and 2.67 on SQLite. PostgreSQL's is the exact answer; on SQLite, a development engine for PormG, the last digit of such a tie can differ (#1061). Round(x) (no places) gives the same whole number on both. MySQL rounds a double half to even, so there a float is rounded as ROUND(CAST(x AS DECIMAL(65,30)), d), half away from zero from the double's shortest digits (#1130); a float of 1e35 or more is clamped by that cast.

The value reads back as its operand's type on every engine (#1121): a Float64 over a float and a Decimal over a decimal, although PostgreSQL computes ROUND(x::numeric, d) as a numeric. Over an integer it is that integer, whatever the places (#1196): PostgreSQL renders ROUND(x::numeric) cast back to the operand's type and SQLite the integer itself, so it reads back as an integer (its width follows the engine, as an integer column's does) and Round("grid") / 2 divides as integers.

Text is not a number to round: Round(x, d) with d ≠ 0 over a text column, a string or a JSON value raises QueryBuildError when the query is built (on SQLite a negative d meets the capability error first), because the query does not say the text holds a number. Cast it first: Round(Cast(x, FloatField()), d).

A negative precision rounds to tens, hundreds, …: Round(125, -1) is 130 on PostgreSQL and MySQL. SQLite's ROUND has no negative precision (it reads one as 0), so there the query raises BackendCapabilityError when it is built (#1044, #1167). Over an integer the result stays that integer type.

See also Rounding.

source
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.

source
PormG.QueryBuilder.SearchHeadline — Method
SearchHeadline(expression, query; config = nothing, start_sel = nothing, stop_sel = nothing,
               max_words = nothing, min_words = nothing, short_word = nothing,
               highlight_all = nothing, max_fragments = nothing, fragment_delimiter = nothing)

expression's text with the words query matches marked (PostgreSQL only): ts_headline, as a String. The expression is cast to text, as each SearchVector field is. query is a SearchQuery, or the search text as a String. config defaults to the query's.

The options are PostgreSQL's (StartSel, StopSel, MaxWords, …), written in snake case. They are checked when the expression is built and sent as one bound parameter: start_sel, stop_sel and fragment_delimiter are strings, highlight_all a Bool, and the rest non-negative integers with 0 < min_words < max_words. Anything else raises InvalidValueError.

M.Race.objects.
  filter("name__@search" => SearchQuery("grand prix"; config = "english")).
  values("year", "hl" => SearchHeadline("name", SearchQuery("grand prix"; config = "english");
                                        start_sel = "<b>", stop_sel = "</b>"))

ts_headline reads the whole document for every row it returns, so filter and limit the rows first. It does not HTML-escape the text: before rendering a headline of user-written text as HTML, see Showing a headline in a web page in the Full-Text Search guide (#1026).

source
PormG.QueryBuilder.SearchQuery — Method
SearchQuery(text; config = nothing, search_type = "plain")

The query side of a full-text search (PostgreSQL only): text parsed into a tsquery. It is the right-hand side of the @search lookup and the query of SearchRank and SearchHeadline.

search_type picks the parser PostgreSQL applies to text:

search_typerenderstext is read as
"plain"plainto_tsquerywords, all required
"phrase"phraseto_tsquerywords, in this order
"websearch"websearch_to_tsquerysearch-engine syntax: "a phrase", or, -word
"raw"to_tsquerytsquery syntax: senn:* & !prost. A syntax error is the server's, at execution

config names a text-search configuration ("english", "simple", "pg_catalog.portuguese"); with none, the server's default_text_search_config applies.

Queries combine with & (both), | (either) and ~ (not), PostgreSQL's &&, || and !!. Combined queries must share one config, or QueryBuildError is raised:

SearchQuery("senna"; config = "simple") | SearchQuery("prost"; config = "simple")
# (plainto_tsquery('simple'::regconfig, $1::text) || plainto_tsquery('simple'::regconfig, $2::text))

The text is bound as a parameter; the config is checked to be a name and written into the SQL, which is what lets PostgreSQL use an index built on to_tsvector('english', col). A config that is not a name, or an unknown search_type, raises InvalidValueError here, as does text containing a NUL character.

M.Driver.objects.filter("surname__@search" => SearchQuery("senna"; config = "simple"))
M.Race.objects.filter("name__@search" => SearchQuery("\"grand prix\" -british"; search_type = "websearch"))
source
PormG.QueryBuilder.SearchRank — Method
SearchRank(vector, query; normalization = nothing, cover_density = false, weights = nothing)

How well each row's document matches a query (PostgreSQL only), as a Float64: ts_rank, or ts_rank_cd with cover_density = true. vector is a SearchVector, or the path of a SearchVectorField column (#1021), a stored document: SearchRank("search", q), Django's SearchRank(F("search"), q). query is a SearchQuery, or the search text as a String, which is parsed with the vector's config (with none, for a stored column, which does not record one). normalization is PostgreSQL's integer bitmask (0 to 63) for weighing the document's length; any other value raises InvalidValueError.

weights scores a word by its SearchVector weight label: four numbers from 0 to 1 for the labels D, C, B and A, in that order, as PostgreSQL's ts_rank takes them (its default is [0.1, 0.2, 0.4, 1.0]). Any other shape raises InvalidValueError. Weights only change scores between words labelled differently, so they go with a weighted vector:

vector = SearchVector("name"; config = "english", weight = "A") +
         SearchVector("location"; config = "english", weight = "B")
SearchRank(vector, SearchQuery("monaco"; config = "english"); weights = [0.0, 0.0, 0.2, 1.0])

A sum of vectors with different configs has no single config to parse a String query with, so it needs a SearchQuery; a String raises QueryBuildError.

Project it under a name, then filter and order by that name:

using PormG.Functions: SearchRank, SearchVector, SearchQuery

M.Driver.objects.
  values("forename", "surname",
         "rank" => SearchRank(SearchVector("forename", "surname"; config = "simple"), "ayrton senna")).
  filter("rank__@gte" => 0.01).
  order_by("-rank")

Filter on a threshold rather than > 0: for a query of several words, a row that misses them can score a tiny positive value (1e-20) instead of 0.

source
PormG.QueryBuilder.SearchVector — Method
SearchVector(fields...; config = nothing, weight = nothing)

The document side of a full-text search (PostgreSQL only): one or more columns, each cast to text, NULL-safe and joined by a space, then parsed into a tsvector. It is the vector of SearchRank.

SearchVector("forename", "surname"; config = "simple")
# to_tsvector('simple'::regconfig, COALESCE(("Tb"."forename")::text, '') || ' ' || COALESCE(("Tb"."surname")::text, ''))

weight labels every word of the document "A", "B", "C" or "D" (setweight), for SearchRank's weights to score; anything else raises InvalidValueError. Two vectors add with + into one document, each keeping its own config and weight:

SearchVector("name"; weight = "A") + SearchVector("location"; weight = "B")
# (setweight(to_tsvector(COALESCE(("Tb"."name")::text, '')), 'A') || setweight(to_tsvector(…), 'B'))

A field is a column path or an expression, as for Lower. A SearchVector is an operand, not a value: projecting one, comparing it or wrapping it in another function raises QueryBuildError, and putting one on the right of a filter pair raises FilterError. To filter a single column, use the @search lookup on it.

source
PormG.QueryBuilder.Sqrt — Method
Sqrt(column)

Returns the square root of a number.

It reads back as a Decimal over a DecimalField and as a Float64 over an integer or a float, on both engines (#1121), as Django's Sqrt types it. PostgreSQL computes it as a numeric.

source
PormG.QueryBuilder.Subquery — Method
Subquery(query::SQLObjectHandler) -> SubqueryObject

Project 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 |> DataFrame

This 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).

It is also a filter value (#926), as in Django's filter(grid=Subquery(...)): filter("milliseconds" => Subquery(fastest)) renders "milliseconds" = (SELECT …), and so do F("milliseconds") == Subquery(fastest), a Q/When condition and a join condition. @in/@nin take the query itself instead, unwrapped.

It is also an update value (#1320), as in Django's update(number=Subquery(...)): update("number" => Subquery(latest)) renders SET "number" = (SELECT …). A subquery that is no number is refused into a number field, as an F expression is (#1297).

Outer `GROUP BY` — guarded (#194)

A correlated subquery that is evaluated after the outer query groups — in the SELECT list or in HAVING — is only well-defined when its correlated column is grouped. When it is not, PormG raises a QueryBuildError naming the ungrouped column, on both backends and before any SQL runs — left to the database, PostgreSQL refuses it while SQLite evaluates the subquery against an arbitrary row of each group and returns a plausible-looking wrong number. A subquery evaluated before grouping is not checked: one in WHERE or a join condition, one inside an aggregate's argument (Sum(Case([When("grid" => Subquery(…), then = 1)], default = 0))), and one inside a projection that is itself a GROUP BY key. See Subqueries and CTEs.

See also Exists for the boolean form and OuterRef for the correlation.

source
PormG.QueryBuilder.Substr — Function
Substr(column, pos, len = nothing)

Returns part of a string: len characters starting at position pos, counted from 1, or every character from pos on when len is omitted. Substr("surname", 1, 3) over "Hamilton" is "Ham", and Substr("surname", 4) is "ilton". A pos past the end gives "", and a NULL string is NULL. The same on PostgreSQL, SQLite and MySQL (SUBSTR), with pos and len bound as parameters.

pos is an integer from 1 and len an integer from 0, both at most 2147483647; anything else — a pos of 0 or below, a negative len, a Bool, a float, a column — raises InvalidValueError when the expression is built. Outside that range the engines disagree: a negative position counts from the end on SQLite and MySQL and gives "" on PostgreSQL, and a negative length raises on PostgreSQL while SQLite returns the characters before pos. Pass the position you mean, counted from the start.

column is a column path or an expression, and it must be text: a number, a date, a time, a timestamp, a boolean, a uuid or a JSON document raises QueryBuildError when the query is built, on every engine. Convert it to text first — an integer, a date, a time or a uuid with Cast (Cast(x, "text")), a timestamp with ToChar, a boolean with a Case. A key inside a JSON document ("payload__name") is text and is accepted.

Filling the driver code from the surname, in one statement:

M.Driver.objects.
    filter("code__@isnull" => true).
    update("code" => Upper(Substr("surname", 1, 3)))
source
PormG.QueryBuilder.Sum — Method
Sum(column; distinct=false)

Computes the sum of all values in the column.

The sum of integers is an integer on both engines. PostgreSQL widens sum(bigint) to numeric, so over a bigint (an IDField, a BigIntegerField, a ForeignKey, an integer literal, integer arithmetic over one) PormG renders SUM(x)::bigint there: Sum("resultid") reads an Int64 and Sum("resultid") / 2 divides as integers on both engines (#1147). A sum past the bigint range raises, as it does on SQLite.

A sum of a DecimalField reads back as a Decimal, and of a FloatField as a Float64, on both engines (#1121): SQLite computes a decimal sum as a double, which PormG reads as the Decimal it means, to 15 significant digits.

A Sum over a BooleanField is refused with a QueryBuildError on every engine (#953): the query does not say which number a flag counts as. Count the true rows explicitly — Sum(When("is_active" => true, then = 1, otherwise = 0)).

A Sum over a duration is the total duration. A Sum over a date, a timestamp, a time or a UUID is refused with a QueryBuildError on every engine (#1387): none is a number. Sum the number you mean, such as Sum(Extract("time", "hour")).

source
PormG.QueryBuilder.ToChar — Method
ToChar(x, format::AbstractString; formatter = nothing)

Format a date/time column as text — PostgreSQL to_char(x, format), SQLite strftime.

Arguments

  • x: a field path, F expression, function object, or a vector of field paths.
  • format: one of the portable formats below, e.g. "YYYY-MM", "YYYY-MM-DD", "YYYY".
  • formatter: an optional Julia-side hook applied to the returned values. Accepts a Function, or a PormGField whose .formatter is used.

Portable formats

Each format renders the same text on both engines for a given instant — PormG spells it for each engine itself (HH is the 24-hour clock on both; the T separator and the .SSS milliseconds render as written). The full list:

formatrenders as
"YYYY", "MM", "DD", "HH", "MI", "SS"one component: 2009, 03, 29, 06, 00, 00
"YYYY-MM", "YYYY-MM-DD"2009-03, 2009-03-29
"DD/MM/YYYY", "DD-MM-YYYY"29/03/2009, 29-03-2009
"HH:MI", "HH:MI:SS", "HH:MI:SS.SSS"06:00, 06:00:00, 06:00:00.000
"YYYY-MM-DD HH:MI:SS", "YYYY-MM-DD HH:MI:SS.SSS"2009-03-29 06:00:00, 2009-03-29 06:00:00.000
"YYYY-MM-DDTHH:MI:SS", "YYYY-MM-DDTHH:MI:SS.SSS"2009-03-29T06:00:00, 2009-03-29T06:00:00.000

A time of day has no date: over a TimeField, a format that writes a year, a month or a day raises QueryBuildError on every engine (#1336). The clock formats ("HH:MI", …) format a time everywhere.

Any other format is PostgreSQL-only

A format outside that table is passed to to_char as written (so a native template such as "HH12:MI AM" works on PostgreSQL), and raises BackendCapabilityError on SQLite and MySQL, naming the supported formats — strftime and DATE_FORMAT cannot spell an arbitrary to_char template.

Time zone

On PostgreSQL to_char renders a timestamptz in the session time zone; on SQLite the stored text is UTC, and on MySQL PormG opens the session in UTC. Keep the PostgreSQL session in UTC for the engines to agree on the hour.

using PormG.Functions: ToChar, Count

# Races per month
query.values("month" => ToChar("date", "YYYY-MM"), "n" => Count("raceid"))

# A race start as the canonical timestamp text, identical on every engine
query.values("start" => ToChar("start_at", "YYYY-MM-DDTHH:MI:SS.SSS"))

Named ToChar since 0.3.0 (previously To_char, with a formater keyword).

See also Functions and Dates.

source
PormG.QueryBuilder.Value — Method
Value(x)

Wraps a literal value for use in SQL queries. It binds as a parameter, except nothing, which renders as NULL.

A string, an integer of any width, a float, a Bool, a Date, DateTime, ZonedDateTime or Time, or a UUID binds on both engines. On SQLite each binds as what its column would store: a date or time as the text a DateField/DateTimeField/TimeField holds, and an integer as Int64. A projected Date, DateTime or Time reads back typed: values("d" => Value(Date(2021, 3, 28))) is a Date, and a DateTime comes back as a UTC ZonedDateTime on SQLite, as a SQLite DateTimeField column does. A ZonedDateTime is an instant: it binds as timestamptz on PostgreSQL and as UTC text on SQLite, and reads back as a ZonedDateTime on both, so Cast(Value(zdt), "date") is the date of that instant in UTC, the session time zone, not the wall-clock date of the offset it was written in. A float binds as a double on SQLite, so a BigFloat or Decimal is narrowed to Float64 there. A value SQLite cannot store as itself (a Symbol, a Rational, an integer beyond Int64, an arbitrary struct) raises InvalidValueError there instead of being bound as a serialized Julia object.

A Sockets.IPv4 / Sockets.IPv6 binds on PostgreSQL as an inet, in the text PostgreSQL prints for it: Value(ip"::FFFF:10.0.0.1") is ::ffff:10.0.0.1. SQLite and MySQL have no network type, so there it raises BackendCapabilityError while the query is built.

update writes Value(x) as x: update("grid" => Value(3)) is update("grid" => 3), with the same checks, formatting and bind (#1320).

using PormG.Functions: Value

"season_label" => Value("2021 season")
source
PormG.QueryBuilder.When — Method
When(condition; then = 0, otherwise = missing)

One WHEN condition THEN value branch of a SQL CASE.

condition accepts five 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
  • a comparison over an F, function or window expression — When(F("grid") < F("positionorder"), then = 1), When(Lower("surname") == "senna", then = 1). It renders exactly as the same expression wrapped in Q(...). An arithmetic or bitwise expression is a value, not a condition, so When(F("laps") + 1) raises QueryBuildError: compare it, as in When((F("laps") + 1) > 0) (#931). A bare boolean column, When(F("is_active")), is a condition.
  • a function whose result is boolean — When(Cast("grid", "boolean"), then = 1), a Coalesce/Case with output_field = "boolean", When(Coalesce(F("is_active"), false)) over a BooleanField. A function whose result is known not to be boolean (Lower, Length, Sum, Rank, a Cast to a non-boolean type, a Coalesce over a number column) raises QueryBuildError: compare it, as in When(Lower("surname") == "senna") (#942). A function whose type cannot be named (Lag over a column, a Case with no output_field) is not checked.

then defaults to 0. A plain value in then or the CASE ELSE is bound as a query parameter; a column expression — F("points"), F("points") * 2, Joined(…), a function — renders as SQL, so When("positionorder" => 1, then = F("points")) returns that row's points. then = missing renders as SQL NULL. (otherwise = missing is the default and means no ELSE of this branch's own — see the tip below.)

`otherwise` makes `When` standalone

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. Outside one, a When with no otherwise raises QueryBuildError wherever it stands as a value (a projection, an aggregate's operand such as Count(When(…)), a function argument), because alone it renders WHEN … THEN … with no ELSE and no END, which no engine parses (#964).

See also Case, Functions and Dates.

source
PormG.QueryBuilder.WindowOver — Function
WindowOver(partition_by, order_by = []; frame = nothing) -> WindowSpec
WindowOver(; partition_by = [], order_by = [], frame = nothing) -> WindowSpec

Build 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, an F expression, or a vector/tuple of them. Symbols are accepted and converted.
  • order_by: ordering inside each window. Strings use the repo-wide "-field" convention for DESC; SQLOrder objects also work, and add an explicit nulls = :first/:last placement, which a string entry has no room for. With nulls unset the window emits no NULLS clause, which is the rendering every window has always produced. Since #509 an SQLOrder can also carry a CTE column — SQLOrder(CTE("name", "col")), or the equivalent "name__col" path, both of which a bare entry has been able to name since #492 — and a Joined column, which only a handle can name. An SQLOrder's direction is its own orientation, so desc = true on a handle nested inside one is refused; pass orientation = "DESC" instead.
  • frame: a frame clause such as "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING". It is SQL grammar rather than a value, so it cannot be a bind parameter: PormG parses it and writes its own spelling back. Accepted: ROWS, RANGE or GROUPS, then one bound or BETWEEN <bound> AND <bound>, optionally followed by EXCLUDE CURRENT ROW | GROUP | TIES | NO OTHERS. A bound is UNBOUNDED PRECEDING, <n> PRECEDING, CURRENT ROW, <n> FOLLOWING or UNBOUNDED FOLLOWING, where <n> is a non-negative integer (under RANGE also a decimal, or INTERVAL '<n> <unit>'). Keywords are case-insensitive. Anything else — including a frame PostgreSQL itself would refuse, such as one whose end comes before its start — raises InvalidValueError when WindowOver is called, on both backends.

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.

`frame` renders on PostgreSQL and MySQL

Passing frame on a SQLite connection raises BackendCapabilityError, and on MySQL a GROUPS frame or an EXCLUDE clause does. Everything else here works on every backend (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.

source
PormG.QueryBuilder.allocate_primary_keys — Method
allocate_primary_keys(objct::SQLObjectHandler, table) -> a table of the same kind

Pre-allocate sequential primary key values for rows in table that are missing an auto-generated primary key, and return a new table with the pk column populated.

table is any Tables.jl table (#1344): a DataFrame, a Vector of NamedTuples, a NamedTuple of vectors, a CSV.File, … The result is the same kind of table, built by the input's own Tables.materializer: a DataFrame for a DataFrame, a Vector of NamedTuples for one, and a NamedTuple of vectors for a source with no materializer of its own (a CSV.File). Its columns are copies, so the caller's table is never modified and element writes on either never reach the other; assign the result (df = allocate_primary_keys(q, df)).

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.

It serves a key the database generates, IDField(). On a model whose key is IDField(generated = false), a key the caller supplies, it raises QueryBuildError whatever table holds: no engine has ids to allocate for such a key, so supply them in the table. A model with no IDField key (a UUIDField or CharField key, or none) gets a copy of table back unchanged.

If table already contains the primary key column with at least one non-blank value, a copy 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 a copy 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).

That counter protects a reservation from SQLite's own auto-id inserts only on an AUTOINCREMENT table, which is what PormG's DDL creates. On a table declared INTEGER PRIMARY KEY without it (an imported or legacy schema), SQLite ignores sqlite_sequence and assigns MAX(rowid) + 1, so a later auto-id insert does not see the reservation. A database with no AUTOINCREMENT table at all has no sqlite_sequence: there the ids start at MAX(pk) + 1 and the reservation lives only in the open transaction. On such a table, allocate and insert inside one run_in_transaction.

Arguments

  • objct: A SQLObjectHandler (typically M.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.
  • table: The Tables.jl table that will be bulk-inserted.

There is no in-place mode (#1344): a generic Tables.jl source cannot be written in place, so the key column is always on the returned table only.

Notes

  • The key column of the returned table holds Int64s (a plain Vector{Int64} in a column table). If the input had a Vector{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_sequence no longer shares the limitation: since #344 it resolves the sequence through the table's own relnamespace.)

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)
end
source
PormG.QueryBuilder.bulk_copy — Method
bulk_copy(objct::SQLObjectHandler, table; 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).
  • table: Any Tables.jl table holding the data to be inserted (a DataFrame, a Vector of NamedTuples, …; #1344).
  • columns: (Optional) Specifies which columns to insert. Can be a String, a Pair{String, String}, or a Vector of these.
  • show_query::Symbol = :execute: A dry-run mode (:sql, :dict, :inspection, :params, :none) returns the COPY command instead of running it (the data stream is not included; :none returns nothing).

The caller's table is never mutated (and a column table is never copied — the pipeline reads its column vectors in place).

Returns

Executed, a NamedTuple (count, rows) (#670): count::Int is the rows copied, summed across chunks and read from each COPY n command tag; rows is always nothing for now. An empty table returns (count = 0, rows = nothing) (nothing under a dry-run show_query).

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)
source
PormG.QueryBuilder.bulk_insert — Method

Inserts multiple rows into the database in bulk from any Tables.jl table (#1344): a DataFrame, a Vector of NamedTuples, a NamedTuple of vectors, a CSV.File, …

Arguments

  • objct::SQLObjectHandler: The SQL object handler to use for the operation.
  • table: The table holding the data to be inserted; any value with Tables.istable(table) == true. Anything else raises QueryBuildError.
  • columns: Optional. Specifies the columns to insert and their mappings. Can be nothing, a String, a Pair{String, String}, or a Vector of these. If nothing, all columns of the table are used.
  • chunk_size::Integer: Optional. The number of rows to insert in each batch (default: 1000). PostgreSQL binds one array per column, so a positive batch is used as given (a non-positive one falls back to the driver's bind-parameter limit ÷ columns rows: 65,535 under LibPQ and Postgres.jl alike); SQLite binds one parameter per cell and caps it automatically to stay under its bind-parameter limit.
  • 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 an ON CONFLICT clause 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/set take logical model field names (resolved through db_column). set fields must participate in the INSERT column list. target is not required to be declared unique in the model — the database is the source of truth for matching constraints. When on_conflict is 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. On MySQL the clause is ON DUPLICATE KEY UPDATE, which names no target: a targeted :nothing or an :update runs only when the target is a unique key the model declares and the one these rows can conflict on, and raises BackendCapabilityError otherwise (#1267).
  • returning: Optional (#671). A field name or a vector of field names whose written values come back in rows — generated primary keys, db_defaults, what a DO UPDATE wrote. Rows are matched to the input by key, never by position: by the on_conflict target when there is one, otherwise by the primary key. A single auto-increment pk the frame does not carry is pre-allocated from its own sequence first (as allocate_primary_keys does), so it can be matched. A model whose pk the database generates by any other means, with no on_conflict target, raises QueryBuildError, as does a target column that is not inserted; an unknown field name raises UnknownFieldError. PostgreSQL reads the rows from RETURNING; SQLite reads them back on the same transaction connection, since PormG never uses SQLite's RETURNING; MySQL, which has none, does the same. MySQL cannot pre-allocate an AUTO_INCREMENT pk, so there that case raises BackendCapabilityError (#1266).

The caller's table is never mutated (and a column table is never copied — the pipeline reads its column vectors in place; a row table is copied into columns once), so there is no copy= knob.

Returns

Executed (show_query = :execute), a NamedTuple (count, rows) (#670):

  • count::Int — rows inserted, summed across every chunk. Rows on_conflict = :nothing skipped are not counted, so the input row count minus count is how many were duplicates. An on_conflict = (action = :update, …) upsert counts every row it inserted or updated.
  • rows — nothing unless returning is given. Then a table of the kind table is, built by its Tables.materializer (#1344): a DataFrame for a DataFrame, a Vector of NamedTuples for one, a NamedTuple of vectors when its package defines no materializer. It has one column per requested field and one row per input row: row i belongs to input row i, and a row this call did not write (skipped by on_conflict = :nothing, or a later duplicate of a key within the frame) is missing — as is a row whose key contains a NULL, which cannot be looked up. A row the database reports that matches no input key raises QueryBuildError instead of going missing.

An empty table returns (count = 0, rows = nothing) — rows is an empty table of the same kind, with the requested columns, when returning is given — and nothing under a dry-run show_query, since there is no statement. Otherwise a dry-run mode returns the statement, as described above; it pre-allocates nothing, so an auto pk it would allocate is absent from the shown column list.

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.

Generated ids, one per input row

r = bulk_insert(query, df; returning = ["id"]) df.id = r.rows.id ```

source
PormG.QueryBuilder.bulk_update — Method

Performs a bulk update operation on a database table using the provided Tables.jl table (a DataFrame, a Vector of NamedTuples, …; #1344) and a query object.

Arguments

  • objct::SQLObjectHandler: The database handler object.
  • table: The table holding the data to be used for the update; any value with Tables.istable(table) == true.
  • columns: (Optional) The participating fields and their mappings — the single place a column of the table is mapped to a model field. Each entry is a String (table column == model field) or a Pair{String, String} of "df_col" => "model_field". A Vector of these is accepted. If nothing, columns are auto-detected from the table. Fields selected by match_on are used for matching only and are not SET.
  • match_on: (Optional) The per-row match keys that identify which row each input row updates (the SQL merge condition Tb.field = source.col). Bare model field names only — the source column is the columns= mapping for that field when you declared one, otherwise a column of the table with the field's own name. A value PormG auto-populates for a field left out of columns= (auto_now, or a static default when columns= is omitted — an explicit columns= 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 raises UnknownFieldError rather than matching every row against one per-call constant. If omitted, the model primary key(s) are used and must be present in the table, under the same precedence. A match key is matched, never written — it stays out of the SET clause, so using an auto_now field as a key does not refresh that timestamp.
  • filters: (Optional) Constant predicates AND'd onto the WHERE clause, applied to every row. Each entry is a Pair{String, T} of "model_field" => value (e.g. "category_id" => 172100, "points__@in" => [18, 25]). When match_on is provided, every filters entry must be such a constant predicate. A per-row match key in filters is rejected with a migration error — move it to match_on.
  • show_query::Symbol: (Optional) :execute (the default) runs the update. The dry-run modes (:sql, :dict, :inspection, :params, :none) build the statement without executing it and return it in that form — one result per chunk, as a Vector when there is more than one chunk (:none returns nothing per chunk).
  • chunk_size::Integer: (Optional) Number of rows to process per chunk. Defaults to 1000. PostgreSQL binds one array per column, so a positive chunk is used as given (a non-positive one falls back to the driver's bind-parameter limit ÷ columns rows: 65,535 under LibPQ and Postgres.jl alike); SQLite and MySQL bind one parameter per cell and cap it automatically to stay under their bind-parameter limit.

Handler scope

Filters already attached to objct are kept: they are AND'd with the match_on= merge condition and the filters= predicates, so a handler scoped with .filter("year" => 1988) only updates rows of that year. The handler itself is never modified — filters= is not written back onto it. A handler carrying state an UPDATE cannot express raises UnsafeMutationError: limit(), offset(), order_by(), distinct(), aggregate annotations, a CTE (.with), a cjoin/on/cjoin_on join, or a filter on a values() alias. A plain values() projection is ignored. A filter that traverses a relation ("driverid__surname" => …) raises QueryBuildError, as the columns do.

The caller's table is never mutated (and a column table is never copied — the pipeline reads its column vectors in place), so the operation is safe in asynchronous contexts without a copy= knob.

Returns

Executed, a NamedTuple (count, rows) (#670):

  • count::Int — rows matched, summed across every chunk: the rows that satisfied the match_on= merge condition, the handler's filters and filters= together. It counts matched rows, as update() does, not only the rows whose values changed. A 0 means nothing matched — say, a frame whose keys belong to a season the handler is not scoped to.
  • rows — always nothing for now; reserved for returned values.

An empty table returns (count = 0, rows = nothing) (nothing under a dry-run show_query).

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 category
source
PormG.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: :execute runs the delete. :sql, :dict, :inspection and :params build the statements and return them instead of executing them, and :none builds and discards them — see show_query. An unrecognized value raises QueryBuildError when 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, _SQLEngine} = 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 the change_data flag, still come from the model's connect_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, :inspection or :params: the built statement(s) — one per statement the plan emits, returned bare when the plan is a single statement and as a Vector otherwise. A cascade can emit several statements for the same table (one UPDATE per SET_NULL / SET_DEFAULT field, plus its DELETE), so the count tracks statements, not tables.
  • Under :none: nothing for each statement, by the same rule — nothing when the plan is a single statement, and a Vector of nothings, one per statement, when it cascades. The statements are built and discarded; on a live pool the key read and the cascade's existence probes still run, as they do for the other inspection modes.

Behavior

Every check below runs before any SQL is generated:

GuardRaises
A transaction is open on a connection other than the model'sTransactionError
The connection is configured change_data: falseWritesDisabledError
The query has limit(), offset() or order_by() setUnsafeMutationError
The query has distinct() setUnsafeMutationError
The query carries group_by() / aggregate annotationsUnsafeMutationError
The query has no filter and allow_delete_all is falseUnsafeMutationError

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: raises ProtectedError, 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: issues UPDATE ... SET <column> = NULL over the dependents instead of deleting them. Declaring it on a null = false field is a contradiction the schema cannot satisfy, and raises ModelDefinitionError.
  • SET_DEFAULT: issues UPDATE ... SET <column> = <the field's default> over the dependents. Declaring it on a field with no default is the mirror-image contradiction, and raises ModelDefinitionError too.
  • 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.

A model reachable by several cascade paths is scoped by all of them: its statement carries one predicate per path, ORed together, and that holds for the UPDATE a SET_NULL / SET_DEFAULT emits just as it does for a DELETE.

Every statement puts the filters on the row being deleted or updated — DELETE FROM "t" AS "Tb" WHERE "Tb"."status" = $1 — rather than only in a pk IN (SELECT …) subquery. A filter that crosses a relation keeps that pk IN (…) as its index-driven selection and adds the same filter as a correlated EXISTS on the row. That makes a filter a fence on PostgreSQL: if a concurrent transaction changes a row so it no longer matches while the delete waits on its lock, the row is left alone (#765). See Filters are a fence on PostgreSQL in Deleting Records.

When the root has a dependent to cascade to (CASCADE, SET_NULL or SET_DEFAULT), a filtered delete resolves the root's primary keys before anything is written: one SELECT "Tb"."<pk>" … WHERE <filter> (FOR UPDATE on PostgreSQL and MySQL), and every statement after it is scoped by those keys (#1342, #1370). Without that, the statements after the first would re-read a filter that the cascade had already changed. A filter that reads another table ("result__grid" => 1, a subquery, an Exists) would see rows the cascade had deleted or re-pointed, and the root could survive its own children. A filter on the root's own columns would see a SET_NULL / SET_DEFAULT update that wrote the root's table, and delete rows the cascade only meant to update. On SQLite and MySQL, which bind a list one value at a time, a large key set runs the cascade in chunks inside the same transaction. A delete with nothing to cascade, and an unfiltered one (allow_delete_all), keep their single statements.

On PostgreSQL and MySQL, before any child statement runs, every collected model that is the parent of an emitted DELETE or UPDATE is locked top-down with SELECT … FOR UPDATE over the same predicate its DELETE uses, so a parent's own columns cannot change after its children are gone (#770). The key read above is the root's lock when it runs. Those statements appear in show_query and inspect_query output as :operation => :lock steps (the key read is a :select step on SQLite). A delete with nothing to cascade emits neither, and SQLite emits no lock at all.

The cascade descends at most 50 levels. Beyond that it raises QueryBuildError naming the models it walked. Usually that means a foreign-key cycle — two models declaring on_delete = CASCADE at each other, or self-referencing rows that form a loop — which would otherwise make the collector descend forever. A genuinely acyclic hierarchy deeper than the ceiling hits it too, and the ceiling is fixed; delete such a graph in stages, from the far end inward.

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. Planning happens inside that transaction too, but on PostgreSQL a concurrent writer can still insert onto a path the planner probed and pruned — see Concurrency in Deleting Records.

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.

source
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.

source
PormG.QueryBuilder.earliest — Method
earliest(objct::SQLObjectHandler, fields...; show_query = :execute) -> PormGRow

Return 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(). On a query with limit() or offset() set it raises QueryBuildError, as Django's does (#1053).

source
PormG.QueryBuilder.explain_query — Method
explain_query(q::SQLObjectHandler; analyze = false, buffers = false, verbose = false, connection = nothing) -> Dict

Ask the database how it would run the SELECT that q builds, and return the plan with its facts. The fluent form is q.explain(; …), as q.inspect() is for inspect_query.

It explains the same statement show_query renders, so it is read-only: writes are terminals (.update(), .delete(), .create()), not something a handler can be explained as.

The result is the inspect_query Dict — :sql_text, :parameters, :dialect, … — plus:

KeyPostgreSQLSQLiteMySQL
:explain_sqlthe EXPLAIN (FORMAT JSON …) statement runthe EXPLAIN QUERY PLAN statement runthe EXPLAIN FORMAT=JSON statement run
:planthe parsed JSON plan documentVector{Dict} of :id, :parent, :detail rowsthe parsed JSON plan document
:indexes_usedevery Index Name in the plan treeeach USING [COVERING] INDEX name; "INTEGER PRIMARY KEY" / "PRIMARY KEY" for a key lookup; "AUTOMATIC INDEX" for a transient index SQLite builds for the statementevery table's key; "PRIMARY" for the primary key
:seq_scanseach relation read by a Seq Scaneach SCAN step that uses no indexeach table read in full (access_type ALL)
:total_cost, :estimated_rowsthe root node's estimatesnothingthe query block's cost, and the rows its last table produces
:planning_time_ms, :execution_time_msmeasured, with analyze = truenothingnothing
:analyzewhether the query was executedalways falsealways false

The facts are what the plan states. PormG does not guess whether an index is missing: a sequential scan of a small table is the right plan.

  • analyze = true executes the query (EXPLAIN ANALYZE) to measure real timing.
  • A select_for_update() query needs a transaction to be explained at all, on every engine — with or without analyze — exactly as it does to run.
  • buffers / verbose add PostgreSQL's BUFFERS / VERBOSE detail to :plan.
  • SQLite has only EXPLAIN QUERY PLAN, and MySQL's EXPLAIN ANALYZE has no format PormG reads: any of the three options raises BackendCapabilityError on both. Each names a table in :seq_scans / :plan by the alias the query gives it.
plan = M.Result.objects.
    filter("driverid__surname" => "Senna").
    values("raceid", "points").
    explain()
plan[:indexes_used]   # ["result_driverid_lunjedbf_idx"] — results found through the driverid index
plan[:seq_scans]      # ["driver"] — the surname filter reads the driver table in full

timed = M.Result.objects.filter("raceid__year" => 1991).values("points").explain(analyze = true)   # PostgreSQL
timed[:execution_time_ms]   # e.g. 0.352

A joined query must still select its columns with .values(...) — explain() builds exactly the statement .list() would run, and refuses the same shapes.

source
PormG.QueryBuilder.inspect_query — Method
inspect_query(q::SQLObjectHandler) -> Dict

Comprehensive 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 inspect
  • operation::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 (:postgresql, :sqlite or :mysql)
    • :model (String): The model/table name
    • :operation (Symbol): The query operation type (:select, :insert, :update, :delete). A cascading :delete returns a Vector of steps; on PostgreSQL that Vector starts with one :lock step (SELECT … FOR UPDATE) per parent model (#770)
    • :bucketing (Symbol): The parameter bucketing strategy (:numbered for PostgreSQL, :positional for SQLite and MySQL)
    • :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 => :numbered
source
PormG.QueryBuilder.latest — Method
latest(objct::SQLObjectHandler, fields...; show_query = :execute) -> PormGRow

Return 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"). A sliced query raises QueryBuildError, as for earliest.

source
PormG.QueryBuilder.list — Method
list(objct::SQLObjectHandler, T::Type; show_query = :execute) -> Vector{T}

The typed read (#1352): every row built into T, a struct or NamedTuple type you declare, so the code after the query is type-stable. With no values(...), the query projects exactly fieldnames(T); with values(...), the projected names (aliases included) must be exactly fieldnames(T), or it raises QueryBuildError before any SQL runs.

struct DriverRow
    driverid::Int64
    surname::String
    code::Union{Nothing, String}
end
rows = M.Driver.objects.filter("nationality" => "Brazilian").order_by("surname").list(DriverRow)

A NULL reads as nothing into a Union{Nothing,X} field and as missing into a Union{Missing,X} one. Integer widths convert, an integer converts to a float or Decimal when exact, and a float widens; any other mismatch, and a NULL into a field that admits neither, raises InvalidValueError naming the field and both types. See Typed rows in the read guide.

source
PormG.QueryBuilder.object — Method
object(model::PormGModel) -> ObjectHandler

Wrap 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...) — add WHERE conditions. Each argument may be a Pair, a Q/Qor, an operator expression, an F expression, or an Exists(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). An alias ("alias" => expr) cannot contain __, the path separator, and raises QueryBuildError (#757)
  • .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 each order_by() clears previous ordering (#199)
  • .limit(n) / .offset(n) — pagination, one clause each. .limit(0) is zero rows, as in SQL; .limit(nothing) removes a limit. A negative value or a Bool raises QueryBuildError, for .page(...) too (#1049)
  • .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) raises QueryBuildError, same as .limit(...) / .offset(...) (#272)
  • .distinct() — add DISTINCT
  • .db("key") — route the query to another connection pool
  • .on(path, pairs...; join_type) — add predicates to the ON clause of a join path, joining it if nothing else in the query does. Adds predicates only: without join_type the join keeps the type derived from the relation itself, and an explicit one stays in effect for later on() calls on that path (#474)
  • .cjoin("field" => "Model"; filters, join_type) — custom join at query time
  • .cjoin_on(model; alias, on, join_type) — anchor-less join where on is the entire ON clause; model is the model object (M.Driver) or its name ("Driver"), and a model registered on another connection is refused (#488). Reference its columns with Joined(alias, column) in any clause. Its alias may equal a relation name on the base model or an on()/cjoin() join path; each stays addressable, and both joins are emitted (#484)
  • .with("name" => subquery; join_field, join_type) — define a CTE; call again for a second one. Reference its columns as "name__column", or as CTE(name, path). Its name may equal a join key and each stays addressable (#474); a name equal to a model field makes the shared __ path raise AmbiguousFieldError, and the handle selects the CTE side (#492)
  • every join_type above accepts "INNER", "LEFT", "RIGHT" or "FULL"; anything else, "CROSS" included, raises QueryBuildError at the call (#474)
  • .select_for_update(; nowait, skip_locked, no_key, of) — SELECT … FOR UPDATE row lock on PostgreSQL (no lock clause on SQLite, where a transaction's BEGIN IMMEDIATE holds the write lock); of limits it to "self", relation paths the query joins, and cjoin_on aliases (FOR UPDATE OF …)
  • .copy() — deep copy, to branch a chain without disturbing the original

Terminal methods

Each executes and returns a result. Every one below except .inspect() and .explain() takes show_query = :sql / :pretty / :dict / :params to render instead of executing; .inspect() is already an inspection call and takes operation = / connection = instead, and .explain() takes analyze = / buffers = / verbose =.

  • .list() → Vector{PormGRow}; .list(:dict) → Vector{Dict}; .list(:json) → JSON String
  • .list(T) → Vector{T} for a struct or NamedTuple type T (the typed read, #1352); .first(T), .last(T), .get(T, pairs...), .earliest(T, f...), .latest(T, f...) likewise
  • query |> DataFrame — preferred for analytical queries
  • .get(pairs...) — exactly one row, or DoesNotExist / MultipleObjectsReturned
  • .first() / .last() — one row or nothing, using the ordering already on the query (.last inverts it, falling back to primary-key descending when none is set)
  • .earliest(fields...) / .latest(fields...) — replace the ordering with fields (ascending / descending) and take one row; at least one field is required, and an empty queryset raises DoesNotExist rather than returning nothing
  • .count(column = nothing; distinct = false) / .exists() — checks without fetching rows
  • .aggregate(pairs...) — whole-queryset aggregation with no GROUP BY; returns a NamedTuple
  • .create(pairs...) — insert one row, returned as a PormGRow
  • .update(pairs...) — update every matching row
  • .get_or_create(lookup...; defaults) / .update_or_create(lookup...; defaults) → (row, created)
  • .delete() — delete every matching row
  • .inspect() — the inspect_query metadata Dict
  • .explain(; analyze = false) — the database's plan for the SELECT, as an explain_query Dict

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.

source
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.

source
PormG.QueryBuilder.resync_sequences — Method
resync_sequences(objct::SQLObjectHandler) -> Vector{String}
resync_sequences(model::PormGModel) -> Vector{String}
resync_sequences(models::AbstractVector{<:PormGModel}) -> Nothing

Explicitly 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 its SQLObjectHandler, 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")
source
PormG.QueryBuilder.save — Method
save(row::PormGRow; show_query=:execute) -> PormGRow | Vector

Persist 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.

source
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.

modeReturns
:sqlString — the generated SQL
:prettyString — the same SQL reflowed one clause per line; only whitespace changes
:paramsVector — the parameterized values, in bucket order
:dict / :inspectionDict — the full metadata shape of inspect_query
:structString — the struct a typed read (list(T)) accepts for this query, as source text to paste (#1632)
:nonenothing — 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 writes nothing into the query you pass (#43, #1624) — 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.

source
PormG.QueryBuilder.validate_value — Method
validate_value(model, field::AbstractString, value) -> the value as PormG formats it for binding

Check value for model's field exactly as a write checks it, without writing anything, and return the value as PormG formats it for binding (validate_value(M.Driver, "dob", Date(1960, 3, 21)) returns "1960-03-21"). Otherwise it raises the same error that M.Model.objects.create(field => value) raises for that field: the same type, with the same kind and the field named.

The checks are the write path's own, run in its order:

  1. The field-name and value checks of validate_field_data.
  2. The bind-time formatting that create, update and the bulk writers run on every value.
  3. The parameter bind of the engine the model is bound to, which has refusals of its own (an integer wider than 64 bits, or an ArrayField value on SQLite). This step runs the engine's parameter encoding (PostgreSQL's text for a byte string, for example) and discards its output.

So a check added to the write path is in this one too. This includes:

  • an unknown field name (UnknownFieldError) or a many-to-many relation;
  • NULL in a NOT NULL field;
  • a value of the wrong type, out of range, or over max_length or a decimal's width;
  • a malformed UUID, date or time;
  • a collection where a column holds one value;
  • a NUL character in text;
  • a value the engine cannot bind.

create's rules apply: a primary key may be given, and a generated column may not. update also refuses a primary key. It checks values, not expressions: update writes an F expression, a SQL function, a Joined column or a Subquery into the statement as SQL and resolves the names inside it when the statement is built, so a double's update passes such a value through without calling this. create does not accept one, and neither does this: for any field, both raise an InvalidValueError of kind :type that names the field and says update writes expressions (#1283). update writes Value(x) as x, while this refuses a Value as create does, so a double's update unwraps it before calling this.

What it does not check is what only the database knows: uniqueness, foreign keys, CHECK constraints, and a row's other fields. It sends nothing. It needs the model's connection key to be registered, as every query does, but not a live database: a key registered with Configuration.register_offline is enough. Use it in a test double that stands in for objects, so the double refuses what PormG refuses (#1239).

source
PormG.Models.CheckConstraint — Method
CheckConstraint(; condition, name)

A table-level CHECK — Django's CheckConstraint. Pass it to Model through constraints =, beside any UniqueConstraints.

condition is SQL, sent to both engines exactly as written, over the table's physical column names (db_column, where a field sets one) and unqualified — grid >= 0, never result.grid >= 0, which would not survive a SQLite table rebuild. It is not a Q(...) expression: a CHECK is DDL, which takes no bind parameters, so there is nothing to translate one into. Like db_default, it is author-written schema text; PormG checks it only for the typos that would silently change the statement it lands in — a -- or /* comment, an unterminated quote, a top-level ; or , — and for the quotes only one engine can see the end of: an E'…' string, a backslash right before a quote, a dollar quote, a backtick (#934).

name is required: it is the constraint's identity. PostgreSQL rewrites a stored condition's text (pg_get_constraintdef re-parenthesises and re-casts it), so a condition is never matched by its text alone. At most 63 bytes, PostgreSQL's limit, on both engines.

Invalid declarations raise ModelDefinitionError: a missing or blank name, a name longer than 63 bytes, a missing or malformed condition here in the constructor; two constraints sharing a name when the model is built.

Diffed by name, changed through a stored marker

PormG stores a hash of the declared condition beside every CHECK it creates — in COMMENT ON CONSTRAINT on PostgreSQL, in an SQL comment inside the constraint on SQLite — and that marker is how makemigrations sees a changed condition (it plans a replace) and knows the constraint is PormG's: one the model no longer declares is dropped. A CHECK written by hand carries no marker and is never planned away — but on SQLite any table rebuild drops it, as it always has, with a warning. Adding, changing or removing a CHECK on SQLite is itself a table rebuild, so it needs migrate(destructive = true); on PostgreSQL a replace or a drop does too. Rows that violate a new condition make the migration fail, on both engines.

Examples

Result::Models.ModelType = Models.Model("result";
  resultid = Models.IDField(),
  grid     = Models.IntegerField(),
  laps     = Models.IntegerField(),
  constraints = [
    Models.CheckConstraint(condition = "grid >= 0 AND grid <= 40", name = "result_grid_range"),
    Models.CheckConstraint(condition = "laps >= 0", name = "result_laps_non_negative"),
  ],
)

See also Model, UniqueConstraint.

source
PormG.Models.Index — Method
Index(; fields, name = nothing, method = "btree", opclasses = nothing, condition = nothing)
Index(; expressions, name, method = "btree", condition = nothing)

Index one or more 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 the indexed 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. A leading - makes that column descending, as in Django: "-points". 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>[_<method>]_idx, where a descending column contributes <col>_desc; the naming rules are UniqueConstraint's.

  • method is the PostgreSQL access method: "btree" (the default), "hash", "gist", "spgist", "gin" or "brin" — a Symbol works too. Only b-tree supports a descending column, and hash and spgist index a single column.

  • opclasses gives each field an operator class, Django's opclasses: one entry per field, with nothing for the column's default class — opclasses = ("jsonb_path_ops",). Each is a lower-case, unqualified identifier, and an index that names one must also have a name.

  • expressions indexes the result of SQL expressions instead of columns — a functional index: expressions = ("lower(surname)",). Give fields or expressions, not both, as in Django.

  • condition makes the index partial: only rows matching it are indexed — condition = "position IS NOT NULL". It combines with either fields or expressions.

  • include makes it a covering index, PostgreSQL's INCLUDE (…): field names whose values the index carries without sorting by them — include = ("points",). It needs a name, works with the btree, gist and spgist methods, and may not repeat a key field. PostgreSQL-only (#934).

method other than "btree", opclasses and include are PostgreSQL features. makemigrations refuses them on SQLite with BackendCapabilityError rather than creating a different index; a descending column works on both engines.

Expression and partial indexes. expressions and condition are SQL, sent to both engines exactly as written — the CheckConstraint contract: over the table's physical column names, unqualified, and not a Q(...) expression, because DDL takes no bind parameters. PormG checks the text only for the typos that would silently change the statement it lands in — a -- or /* comment, an unterminated quote, a top-level ; or , (so each entry of expressions is exactly one index member). Both engines support both kinds; write SQL both understand when a model runs on both.

  • Each expression is one index member, written as PostgreSQL's index-element syntax: a function call ("lower(surname)"), or any other expression in parentheses ("(points * 2)"). A collation, an operator class or a direction goes inside the text — "surname COLLATE "C"", "lower(surname) text_pattern_ops", "lower(surname) DESC" — so opclasses is refused beside expressions. PostgreSQL also requires every function in it to be IMMUTABLE.
  • name is required, as in Django: a derived name could not say what the text indexes.
  • Changing the text is a drop and a create of the index, which migrate treats as destructive. PostgreSQL stores a rewritten form of the text (lower(surname::text)), so PormG cannot compare your declaration with it; it stores a hash of the declared text in the index's ownership marker, pormg:index:<hash>, and compares that.
  • Renaming or removing a column the text names is refused by makemigrations with InvalidMigrationError until the text no longer names it: PormG does not rewrite your SQL.
One plain column is `db_index = true`, not a one-field `Index`

A single-column Index that is b-tree, ascending and uses the default operator class is rejected. It is unrepresentable in both directions: such a CREATE INDEX is byte-identical whether db_index = true or an Index emitted it, so introspection reads it back as db_index, and a model declaring it would compare unequal to its own live table forever. Declare db_index = true on the field instead. A one-column index with a method, a - or an operator class is a different index, and is accepted — and so is a partial one, which its condition makes different, and a covering one, which its include does.

Invalid declarations raise ModelDefinitionError as early as they can be detected: a plain index on fewer than two fields, a repeated field ("lap" and "-lap" repeat it), an unknown method, a descending column on a method other than "btree", more than one member on hash or spgist, both or neither of fields and expressions, a blank or malformed expression or condition, either one without a name, opclasses beside expressions, opclasses of the wrong length or shape, opclasses without a name, an include without a name, repeating a key field or itself, or beside a method other than btree/gist/spgist, 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 another index's — a UniqueConstraint's, or one on another table — fails at migration planning, where every name is known.

Diffed like a column

On a table that already exists makemigrations plans adding, removing, changing and renaming an Index (#161), exactly as for UniqueConstraint. A plain index the models file does not declare is planned for removal. An index with a method, a descending column or an operator class is removed only when PormG created it: PormG marks every such index it creates, and one written by hand is never planned away — declaring it adopts it. inspectdb reads all of them back, so a model generated from a live database declares every one.

Examples

Result::Models.ModelType = Models.Model("result",
  resultid = Models.IDField(),
  raceid   = Models.ForeignKey(Race, pk_field = "raceid", on_delete = "CASCADE"),
  driverid = Models.ForeignKey(Driver, pk_field = "driverid", on_delete = "RESTRICT"),
  points   = Models.FloatField(),
  indexes = [
    Models.Index(fields = ("raceid", "-points"), name = "result_race_points_idx"),
  ],
)

which migrates to:

CREATE INDEX "result_race_points_idx"
  ON "result" ("raceid", "points" DESC);

On PostgreSQL that statement is followed by COMMENT ON INDEX "result_race_points_idx" IS 'pormg:index'; — the ownership marker. A prefix search on a driver's surname, under a database locale other than C, needs the pattern operator class:

Models.Index(fields = ("surname",), opclasses = ("varchar_pattern_ops",),
             name = "driver_surname_pattern_idx")

See also Model, UniqueConstraint.

source
PormG.Models.ModelSet — Type
ModelSet

The model set a models module declares: its models and their binding names, in declaration order, the module, and the registration set_models records for it. Written by @models as the module's typed global MODEL_SET, and read by set_models instead of scanning the module's names (#1602). Build it with @models, not by hand. A juliac --trim application passes it to registration itself — set_models(models.MODEL_SET; key = "f1") — because a trimmed image keeps only the bindings its code references.

The record (key, load_root, folder) is filled by set_models (#1607): the connection key the module was bound to, the root that key was loaded relative to (#857), and the models folder. It lives in this object, created by the module's own body, so registration MUTATES it and writes nothing into the module after it has closed. A package image saves it with the module, which is how a restored image rebinds to the key it was imported under (#1227).

source
PormG.Models.ModelType — Type
ModelType <: PormGModel

The concrete model object — what Model returns and what M.Driver is. It is the only concrete subtype of PormGModel. Build models with Model(...) rather than calling this constructor; name the type where a model is bound, so the binding has a type the compiler can rely on:

Driver::Models.ModelType = Models.Model(
  driverid = Models.IDField(),
  surname  = Models.CharField(max_length = 255),
)

Construction fills name, fields and field_names; the remaining slots are populated later by set_models (directly or through @import_models), which is why a model used before registration has connect_key === nothing and no reverse relations.

SlotFilled byHolds
nameModel(...), or set_models from the Julia bindingThe table name. For a model you declare it is always lowercase — a positional name is rejected unless already lowercase (#300), a binding-derived one is lowercased as it is filled in. A model built by inspectdb introspection or the Django importer instead keeps the name read from its source, mixed case and all
managedModel(managed = false)false when PormG queries the table but never migrates it (#741). Default true
fieldsModel(...)Declared field name → PormGField, including many-to-many fields
field_namesModel(...)The subset that owns a real column — many-to-many fields are excluded
related_objectsset_modelsReverse accessors installed by relations pointing at this model: accessor name → a ReverseRelation (a foreign key on another model) or a ManyToManyRelation (a many-to-many, reversed). Each carries the resolved child model, so a reverse join never re-derives a Julia binding from a name (#343)
_module / connect_keyset_modelsThe defining module and the connection key it registered under
cacheset_models, Model(constraints = …, indexes = …), introspectionDerived metadata, one typed slot per kind: forward many-to-many relations, declared unique and check constraints, declared composite indexes, and what a model read from a database records about its live indexes

deepcopy shares rather than clones (deepcopy(model) === model): a model is resolved schema state treated as an immutable shared reference, and recursion would otherwise descend into _module::Module and throw. The query builder relies on this — copying a query copies its state but keeps the same model.

See also Model, set_models, UniqueConstraint, Index.

source
PormG.Models.UniqueConstraint — Method
UniqueConstraint(; fields, name = nothing)
UniqueConstraint(; fields, condition, name)
UniqueConstraint(; expressions, condition = nothing, name)

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.

condition and expressions are Django's: a partial unique constraint, enforced only on the rows the condition matches (condition = "position IS NOT NULL" over a sprint's (raceid, position) — one classified finisher per position, retirements aside), and a functional one, unique over SQL expressions (expressions = ("lower(driverref)",)). Both are SQL text under the Index contract — physical unqualified columns, checked only for what would change the statement — fields and expressions are exclusive, and either needs a name. Each becomes a CREATE UNIQUE INDEX … WHERE … on both engines, owned through the hashed pormg:index:<hash> marker as an expression or partial Index is (#934).

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, and accepts whatever an existing index over the same columns is called. Given, it is intent: an existing index under another name is renamed to it. Names are unique per PostgreSQL schema and per SQLite database, and PostgreSQL keeps only 63 bytes of one — makemigrations refuses a plan that would create one name twice, as stored, or create one another table's index still holds.

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.

Diffed like a column

Each constraint becomes a CREATE UNIQUE INDEX — identical on PostgreSQL and SQLite. On a table that already exists makemigrations plans adding, removing, re-columning and renaming one (#161), and matches it to the live schema by its columns, so a UNIQUE (…) constraint adopted from Django satisfies it. A composite the model does NOT declare is planned for removal, which migrate treats as destructive.

Examples

Constructor_engine::Models.ModelType = 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 "uniq_constructor_year"
  ON "constructor_engines" ("constructorid", "year");

See also Model.

source
PormG.Models.ArrayField — Method
ArrayField(base_field; size = nothing, kwargs...)

A one-dimensional PostgreSQL array of base_field values — Django's ArrayField, from django.contrib.postgres.fields. ArrayField(Models.CharField(max_length = 10)) is a character varying(10)[] column.

Values are written and read as a Julia Vector. A read returns Vector{T}, T being what a scalar read of the base field returns (Int32 for IntegerField, String for the text fields and UUIDField, Decimal for DecimalField, a UTC ZonedDateTime for a timestamptz), or Vector{Union{Missing, T}} when an element is NULL. Each element is validated as the base field validates a value, before the server sees it. A NULL element is refused unless the base field is declared null = true.

PostgreSQL only. SQLite has no array type, and PormG does not emulate one: creating or altering a SQLite table with this field raises BackendCapabilityError.

The element field describes an element, so it takes only null and its type modifiers (max_length, max_digits, decimal_places, type); a column keyword on it (unique, db_index, default, …) is refused with FieldValidationError. Elements may be CharField, TextField, SlugField, EmailField, URLField, IntegerField, BigIntegerField, FloatField, DecimalField, BooleanField, DateField, DateTimeField or UUIDField. A nested ArrayField is not supported.

Keyword Arguments

  • size::Union{Int, Nothing} = nothing: the most elements a value may have, checked by PormG on every write (PostgreSQL does not enforce a declared size, and does not keep it, so it is not part of the schema)
  • verbose_name::Union{String, Nothing} = nothing: A human-readable name for the field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values (the whole array; see above for elements)
  • db_index::Bool = false: Whether to create a database index on this field
  • default = nothing: Default array — a Vector or a Tuple (default = Int[] is Django's default = list), or an array literal String. Stored as its canonical literal ("{1,2}"), so it is never a shared mutable vector. A function is refused
  • db_default::Union{NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). Pin it to PostgreSQL — (postgres = "ARRAY[]::integer[]",). Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = true: Whether the field should be editable in forms

Database Mapping

  • PostgreSQL Type: the base field's type followed by [] (integer[], character varying(10)[])
  • SQLite: none — rendering the column raises BackendCapabilityError (see above)

Examples

Race_strategy::Models.ModelType = Models.Model("race_strategy",
  id             = Models.IDField(),
  raceid         = Models.ForeignKey("Race"),
  tyre_compounds = Models.ArrayField(Models.CharField(max_length = 12); size = 6),
  pit_laps       = Models.ArrayField(Models.IntegerField(), default = Int[]),
)
source
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 the upgrade log (PormG.upgrade_guide).

Migration

# before
Part_category::Models.ModelType = Models.Model(
    id   = Models.AutoField(),
    name = Models.CharField(max_length = 100)
)

# after
Part_category::Models.ModelType = 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 the upgrade log — 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.

source
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 field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{Int64, Nothing} = nothing: Default value for the field
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::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.ModelType = 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.ModelType = 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.ModelType = 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

  1. Large Identifiers: External API IDs, social media IDs
  2. Timestamps: Unix timestamps in milliseconds or microseconds
  3. Population Data: Country populations, large counts
  4. Financial Data: Large monetary values in smallest units
  5. Scientific Data: Large measurements, particle counts
  6. 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
source
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 a CHECK constraint in the DDL — octet_length on PostgreSQL, length on SQLite. nothing means unbounded. At most 1073741824 (1 GiB, PostgreSQL's limit for one bytea value); a larger bound raises FieldValidationError, because it would constrain nothing.
  • default::Union{Vector{UInt8}, Nothing} = nothing: rendered into the DDL as a byte literal
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide ('\x…'::bytea / X'…'). Must be a Vector{UInt8}; a String raises FieldValidationError rather 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.ModelType = 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 reads a blob written by another Julia process

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.

See also FileField, TextField, CharField.

source
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 field
  • unique::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 forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{Bool, Nothing} = nothing: Default value for the field (true or false)
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = false: Whether the field should be editable in forms

Examples

Basic boolean flags:

User::Models.ModelType = 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, accepts NULL/nothing
source
PormG.Models.CIDRField — Method
CIDRField(; kwargs...)

One IPv4 or IPv6 network in CIDR notation — PostgreSQL's native cidr.

Values are read and written as a String; a Sockets.IPv4 or Sockets.IPv6 is accepted on write as a one-address network. A value is written in the text PostgreSQL prints for it, always with its prefix: a value without one is the full-width network (10.0.0.1 is stored as 10.0.0.1/32).

PostgreSQL only, as GenericIPAddressField is: on SQLite, rendering the column raises BackendCapabilityError.

A value with bits set to the right of its mask raises InvalidValueError, as PostgreSQL refuses it: 10.0.0.1/24 is a host inside 10.0.0.0/24, and the message names that network.

Keyword Arguments

  • verbose_name::Union{String, Nothing} = nothing: A human-readable name for the field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{String, Nothing} = nothing: Default network, normalized like a written value
  • db_default::Union{NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). Pin it to PostgreSQL. A fixed network is a literal, not an expression: declare it as default, since a literal db_default ('10.0.0.0/8'::cidr) is refused (#1033). The portable spellings ("CURRENT_TIMESTAMP", …) are not addresses. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = true: Whether the field should be editable in forms

Database Mapping

  • PostgreSQL Type: cidr
  • SQLite: none — rendering the column raises BackendCapabilityError (see above)

Examples

Team_network::Models.ModelType = Models.Model("team_network",
  id       = Models.IDField(),
  team     = Models.CharField(max_length = 100),
  subnet   = Models.CIDRField(unique = true),
)

See also GenericIPAddressField.

source
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 field
  • max_length::Int = 250: Maximum number of characters allowed (1 or greater)
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • db_column::Union{String, Nothing} = nothing: Map this field to a differently-named physical column (Django db_column). Authoritative across DDL, queries, and migrations (#50); defaults to the field name
  • default::Union{String, Nothing} = nothing: Default value for the field
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • choices::Union{NTuple{N, Tuple{AbstractString, AbstractString}}, Nothing} = nothing: Restricted set of valid values
  • editable::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 varchar accepts 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.ModelType = 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.ModelType = 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.ModelType = 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.ModelType = 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.ModelType = Models.Model(
    id = IDField(),
    title = CharField(max_length=200),
    priority = CharField(max_length=10, choices=priority_choices, default="medium")
)

Choice Format Options:

  1. Tuple of Tuples: (("value1", "Display 1"), ("value2", "Display 2"))
  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

FeatureCharFieldTextField
LengthBounded (max_length)Unlimited
Database TypeVARCHARTEXT
Use CaseShort stringsLong content
IndexingEfficientLess efficient
PerformanceFast queriesSlower for large content

Migration Considerations

  • Increasing Length: Safe operation
  • Decreasing Length: Requires data validation
  • Adding Choices: Application-level change only
  • Changing Column Name: the physical column is db_column when set, else the field name, and db_column is authoritative across DDL, queries and the migration diff (#50). The diff compares physical columns, so renaming the field while db_column pins the old name plans nothing. Changing the physical name — renaming an unpinned field, or changing db_column — reads as one column gone and another added: interactive makemigrations asks whether they are the same field and plans a RENAME COLUMN if you say so; otherwise it plans ADD COLUMN plus DROP COLUMN, which loses the column's data. See Renaming a field in the migrations guide

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=true default 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

  • TextField for unlimited length text content
  • EmailField for email address validation
  • Database design best practices for string field sizing
source
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 field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{Date, Nothing} = nothing: Default value for the field. The four accepted input spellings are listed under Date Input Formats below; all of them are stored as a Date
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = false: Whether the field should be editable in forms
  • auto_now::Bool = false: Whether to automatically set to current date on every save
  • auto_now_add::Bool = false: Whether to automatically set to current date on creation only

Examples

Basic date fields:

User::Models.ModelType = 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.ModelType = 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.ModelType = 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 creation

auto_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 dates

Date Input Formats

default= accepts all four spellings and stores a Date for every one of them (#631 — three of these raised a raw MethodError until the default= converter stopped being the SQL formatter):

  • Julia Date: Date(2024, 7, 28)
  • DateTime: DateTime(2024, 7, 28, 10, 30) — the time is dropped
  • ZonedDateTime: its LOCAL calendar date
  • String: anything Date(::AbstractString) parses, i.e. "2024-07-28". A malformed separator or an impossible calendar date ("2023-02-29") raises FieldValidationError
source
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: nothing
  • unique::Bool: If true, ensures field values are unique across the table. Default: false
  • blank::Bool: If true, allows empty values in forms/validation. Default: false
  • null::Bool: If true, allows NULL values in the database. Default: false
  • db_index::Bool: If true, creates a database index for faster queries. Default: false
  • default::Union{DateTime, Nothing, String}: Default value for the field. Can be a DateTime object, ISO string, or nothing. Default: nothing
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool: If true, field can be edited in forms. Default: false
  • auto_now::Bool: If true, automatically updates to current datetime on every save. Default: false
  • auto_now_add::Bool: If true, automatically sets to current datetime when record is created. Default: false
  • type::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)```
source
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: nothing
  • unique::Bool: If true, ensures field values are unique across the table. Default: false
  • blank::Bool: If true, allows empty values in forms/validation. Default: false
  • null::Bool: If true, allows NULL values in the database. Default: false
  • db_index::Bool: If true, creates a database index for faster queries. Default: false
  • default::Union{Float64, Nothing}: Default value for the field. Default: nothing
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool: If true, field can be edited in forms. Default: false
  • max_digits::Int: Maximum number of digits allowed (including decimal places). Default: 10. At most 15 on SQLite, which has no exact decimal type: its NUMERIC affinity keeps only 15 significant digits, so makemigrations raises BackendCapabilityError for a wider declaration rather than create a column that would round values as they are written (#648). PostgreSQL accepts any width.
  • decimal_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
)
source
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.ModelType = 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.

source
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: nothing
  • unique::Bool: If true, ensures field values are unique across the table. Default: false
  • blank::Bool: If true, allows empty values in forms/validation. Default: false
  • null::Bool: If true, allows NULL values in the database. Default: false
  • db_index::Bool: If true, creates a database index for faster queries. Default: false
  • default::Union{String, Nothing}: Default email address. Default: nothing
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool: If true, 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, 
)
source
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.

source
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: nothing
  • unique::Bool: If true, ensures field values are unique across the table. Default: false
  • blank::Bool: If true, allows empty values in forms/validation. Default: false
  • null::Bool: If true, allows NULL values in the database. Default: false
  • db_index::Bool: If true, creates a database index for faster queries. Default: false
  • default::Union{Float64, String, Int64, Nothing}: Default value for the field. Default: nothing
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool: If true, 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)
source
PormG.Models.ForeignKey — Method
ForeignKey(to::Union{AbstractString, 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{AbstractString, 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 field
  • primary_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 forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_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)
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = false: Whether the field should be editable in forms
  • pk_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 deleted
  • how::Union{String, Nothing} = nothing: Join type for queries ("INNER JOIN", "LEFT JOIN", etc.)
  • related_name::Union{String, Nothing} = nothing: Name for the reverse relation
  • db_constraint::Bool = true: Whether to create a database foreign key constraint
  • db_column::Union{String, Nothing} = nothing: Map the local FK column to a differently-named physical column (#50). The referenced parent column follows pk_field (resolved through the parent field's own db_column when the target is a resolved model). Defaults to the field name

Database Mapping

  • PostgreSQL Type: BIGINT with foreign key constraint
  • Constraint: Creates FOREIGN KEY constraint 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 deleted
  • RESTRICT: Prevent deletion of referenced object if this object exists
  • SET_NULL: Set this field to NULL (requires null=true)
  • SET_DEFAULT: Set this field to its default value (requires default to be set)
  • PROTECT: Raise an error to prevent deletion
  • DO_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.ModelType = 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.ModelType = 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.ModelType = 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_name is not specified, PormG derives one and logs it at @info
  • The derivation counts every relation this model declares to that target — ForeignKey, OneToOneField and ManyToManyField alike: 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_name stays nothing unless 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 ModelDefinitionError at set_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_objects or your_model.related_objects to 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 to parameter must be a valid model name or PormGModel instance
  • All boolean parameters are validated for type safety
  • The on_delete parameter is validated against allowed values
  • A keyword this field does not accept raises FieldValidationError (#1333); on_update, deferrable and initially_deferred, retired in #516, name their own reason. Only the schema-neutral keywords listed under Keywords a field does not accept in the fields guide are warned about and ignored

Notes

  • The field stores the primary key value of the referenced object
  • Uses BIGINT type to match IDField primary keys
  • Deferrability is not configurable (#516): PostgreSQL emits every foreign-key constraint DEFERRABLE INITIALLY DEFERRED, and SQLite renders no deferrability clause at all
  • No ON UPDATE clause is rendered on either backend

See Also

  • Django's ForeignKey documentation for conceptual understanding
source
PormG.Models.GenericIPAddressField — Method
GenericIPAddressField(; kwargs...)

One IPv4 or IPv6 host address — PostgreSQL's native inet. Django's field of the same name, so a Django project imports into it unchanged.

Values are read and written as a String; a Sockets.IPv4 or Sockets.IPv6 is accepted on write. PormG validates a value before it is sent and writes it in the text PostgreSQL prints for it — IPv6 compressed and lower-cased, 0:0:0:0:0:ffff:a00:1 as ::ffff:10.0.0.1 — which is also the form a default= must take to match the catalog.

PostgreSQL only. SQLite has no type that stores an address with inet's semantics, and PormG does not emulate one: creating or altering a SQLite table with this field raises BackendCapabilityError.

A value with a /prefix is refused with InvalidValueError: this field holds a host, and a network belongs in a CIDRField. An empty string is refused too; store nothing in a null = true field instead.

Keyword Arguments

  • protocol::String = "both": "both", "IPv4" or "IPv6" (any case). A write of the other family raises InvalidValueError
  • unpack_ipv4::Bool = false: store an IPv4-mapped address (::ffff:10.0.0.1) as its IPv4 form (10.0.0.1). Only with protocol = "both"
  • verbose_name::Union{String, Nothing} = nothing: A human-readable name for the field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{String, Nothing} = nothing: Default address, normalized like a written value
  • db_default::Union{NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). Pin it to PostgreSQL — (postgres = "inet_client_addr()",). A fixed address is a literal, not an expression: declare it as default, since a literal db_default ('10.0.0.1'::inet) is refused (#1033). The portable spellings ("CURRENT_TIMESTAMP", …) are not addresses. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = true: Whether the field should be editable in forms

Database Mapping

  • PostgreSQL Type: inet
  • SQLite: none — rendering the column raises BackendCapabilityError (see above)

Examples

Pit_wall_session::Models.ModelType = Models.Model("pit_wall_session",
  id          = Models.IDField(),
  team        = Models.CharField(max_length = 100),
  client_ip   = Models.GenericIPAddressField(),
  relay_ip    = Models.GenericIPAddressField(protocol = "IPv4", null = true),
  garage_lan  = Models.CIDRField(),
)

See also CIDRField.

source
PormG.Models.IDField — Method
IDField(; kwargs...)

A 64-bit integer primary key, generated by the database by default.

generated is the keyword that controls identity, on every engine. generated = true (the default) is a key the database generates: GENERATED BY DEFAULT AS IDENTITY on PostgreSQL (GENERATED ALWAYS AS IDENTITY with generated_always = true), INTEGER PRIMARY KEY AUTOINCREMENT on SQLite, AUTO_INCREMENT on MySQL. generated = false is a key the caller supplies (a source system's id): no engine fills it, so an insert without it is refused. On SQLite the column is then BIGINT, because an INTEGER PRIMARY KEY is a rowid alias, which SQLite fills with or without AUTOINCREMENT (#1393).

Keyword Arguments

  • verbose_name::Union{String, Nothing} = nothing: A human-readable name for the field
  • primary_key::Bool = true: Whether this field is the primary key for the table
  • unique::Bool = true: Whether values in this field must be unique across all records
  • blank::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 values
  • db_index::Bool = true: Whether to create a database index on this field
  • default::Union{Int64, Nothing} = nothing: Default value for the field (rarely used with a generated key)
  • editable::Bool = false: Whether the field should be editable in forms (typically false for ID fields)
  • generated::Bool = true: Whether the database generates the key. false declares a key the caller supplies, on every engine: allocate_primary_keys refuses it, and bulk_insert treats a blank key as a missing value rather than leaving the column out
  • generated_always::Bool = false: GENERATED ALWAYS AS IDENTITY on PostgreSQL, which refuses an explicit key value; a no-op on SQLite and MySQL

auto_increment is refused (#1334). It was stored and never rendered on any engine; use generated = false for a key the database does not generate.

Database Mapping

DeclarationPostgreSQLSQLiteMySQL
IDField()bigint … GENERATED BY DEFAULT AS IDENTITYINTEGER PRIMARY KEY AUTOINCREMENTBIGINT … AUTO_INCREMENT PRIMARY KEY
IDField(generated_always = true)bigint … GENERATED ALWAYS AS IDENTITYsame as IDField()same as IDField()
IDField(generated = false)bigint PRIMARY KEY UNIQUE NOT NULLBIGINT PRIMARY KEY UNIQUE NOT NULLBIGINT NOT NULL PRIMARY KEY

Examples

Driver::Models.ModelType = Models.Model(
  driverid = Models.IDField(),
  surname = Models.CharField(max_length = 255)
)

# A key the caller supplies, e.g. copied from another system
Circuit_source::Models.ModelType = Models.Model("circuit_source",
  circuitid = Models.IDField(generated = false),
  name = Models.CharField(max_length = 255)
)

Validation

  • All boolean parameters are validated to ensure type safety
  • The verbose_name must be a String or nothing
  • The default value, if provided, must be convertible to Int64
  • A keyword IDField does not accept raises FieldValidationError (#1333), except the schema-neutral ones listed under Keywords a field does not accept in the fields guide
source
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: nothing
  • unique::Bool: If true, ensures field values are unique across the table. Default: false
  • blank::Bool: If true, allows empty values in forms/validation. Default: false
  • null::Bool: If true, allows NULL values in the database. Default: false
  • db_index::Bool: If true, creates a database index for faster queries. Default: false
  • default::Union{String, Nothing}: Default image path or URL. Default: nothing
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool: If true, 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
)
source
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 field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{Int64, Nothing} = nothing: Default value for the field
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = false: Whether the field should be editable in forms

Examples

Basic integer field:

Product::Models.ModelType = 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.ModelType = 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.ModelType = 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
source
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 field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{String, Nothing} = nothing: Default JSON value as a string
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = true: Whether the field should be editable in forms

Database Mapping

  • PostgreSQL Type: JSONB
  • SQLite Type: TEXT

Examples

Race::Models.ModelType = 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.
source
PormG.Models.ManyToManyField — Method
ManyToManyField(to::Union{AbstractString, 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 a ForeignKey'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 when through is given — the join table is then the through model's own table (its db_table if it declares one).
  • source_field::Union{String, Nothing} = nothing: the join key pointing at the source model. With an explicit through, it names a field on that model and the physical column is resolved from the field's db_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.

source
PormG.Models.Model — Method
Model(; constraints = nothing, db_table = nothing, indexes = nothing, managed = nothing, fields...)
Model(name; constraints = nothing, db_table = nothing, indexes = nothing, managed = nothing, fields...)

Define a model — one database table, described by its fields. Returns a ModelType: 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.

A positional name must be lowercase and may not start with '_'

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, and CheckConstraint objects for table-level CHECKs (#742), in one list. 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.ModelType = 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, or one with a descending column, a PostgreSQL access method or operator classes (#29). A plain single-column index stays the field option db_index = true.

managed = false (#741) marks a model PormG queries but never migrates — a view, or a table another system owns — Django's Meta.managed. The migration planner never creates, alters, renames or drops its table, and never offers it as a rename candidate; queries, joins and __ traversal work exactly as for any other model. A foreign key from a managed model to an unmanaged one must say db_constraint = false, because the target may be a view and a view cannot be referenced:

DriverStandingsV::Models.ModelType = Models.Model("driver_standings_v"; managed = false,
  driverstandingsid = Models.IDField(),
  driverid = Models.ForeignKey(Driver, pk_field = "driverid", db_constraint = false),
  points   = Models.FloatField(),
)

constraints, db_table, indexes and managed are the only model-level options.

`constraints`, `db_table`, `indexes` and `managed` are not field names

All four 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.

PormG has no Django `Meta` block

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.ModelType = 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.ModelType = 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.ModelType = 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.ModelType = Models.Model("constructor_standings",
  db_table = "Constructor_Standings",
  id = Models.IDField(),
)

See also set_models, UniqueConstraint, Index, ForeignKey.

source
PormG.Models.OneToOneField — Method
OneToOneField(to::Union{AbstractString, 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{AbstractString, 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 field
  • primary_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 forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_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)
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = false: Whether the field should be editable in forms
  • pk_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 deleted
  • how::Union{String, Nothing} = nothing: Join type for queries ("INNER JOIN", "LEFT JOIN", etc.)
  • related_name::Union{String, Nothing} = nothing: Name for the reverse relation
  • db_constraint::Bool = true: Whether to create a database foreign key constraint
  • db_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 KEY constraint with UNIQUE constraint
  • 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 deleted
  • RESTRICT: Prevent deletion of referenced object if this object exists
  • SET_NULL: Set this field to NULL (requires null=true)
  • SET_DEFAULT: Set this field to its default value (requires default to be set)
  • PROTECT: Raise an error to prevent deletion
  • DO_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.ModelType = Models.Model(
    id = IDField()
    username = CharField(max_length=150, unique=true)
    email = EmailField()
)

UserProfile::Models.ModelType = 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.ModelType = Models.Model(
    id = IDField()
    name = CharField(max_length=100)
    department = CharField(max_length=50)
)

EmployeeSettings::Models.ModelType = 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.ModelType = Models.Model(
    id = IDField()
    name = CharField(max_length=200)
    price = DecimalField(max_digits=10, decimal_places=2)
)

ProductDetails::Models.ModelType = 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 prefer, for readability and because it is what introspection reports for such a column, on both PostgreSQL and SQLite (#417).

It is a preference, not a requirement. Since #437 the migration planner converges the two spellings against the same live column — it diffs them attribute by attribute rather than by struct type — so an existing ForeignKey(..., unique=true) declaration needs no rewrite and proposes no migration. (Before #437 it proposed one on every makemigrations as soon as any other column in that table changed: an ALTER that re-rendered the column unchanged, and a full table rebuild on SQLite.)

Validation

  • The to parameter must be a valid model name or PormGModel instance
  • All boolean parameters are validated for type safety
  • The on_delete parameter is validated against allowed values
  • Uniqueness is automatically enforced at the database level
  • A keyword this field does not accept raises FieldValidationError (#1333); on_update, deferrable and initially_deferred, retired in #516, name their own reason. Only the schema-neutral keywords listed under Keywords a field does not accept in the fields guide are warned about and ignored

Notes

  • Deferrability is not configurable (#516): PostgreSQL emits every foreign-key constraint DEFERRABLE INITIALLY DEFERRED, and SQLite renders no deferrability clause at all
  • No ON UPDATE clause is rendered on either backend

See Also

  • Django's OneToOneField documentation for conceptual understanding
  • Database normalization principles for when to use one-to-one relationships
source
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 field
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • editable::Bool = true: Whether the field should be editable in forms
  • max_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$base64encodedHash

Where:

  • pbkdf2_sha256: Algorithm identifier
  • 720000: Number of iterations
  • salt: Random 22-character salt
  • base64encodedHash: The derived key in base64

Examples

Basic password field:

# Define a User model with password
User::Models.ModelType = 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

  • CharField for generic string storage
  • Django's password management documentation
source
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 field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{Int64, Nothing} = nothing: Default value for the field (must be 0..2147483647)
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • 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.ModelType = Models.Model(
    id = IDField(),
    lap = PositiveIntegerField(default=1),
    milliseconds = PositiveIntegerField()
)
source
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 field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{Int64, Nothing} = nothing: Default value for the field (must be 0..32767)
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • 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.ModelType = Models.Model(
    id = IDField(),
    position = PositiveSmallIntegerField(default=1),
    points = PositiveSmallIntegerField(default=0)
)
source
PormG.Models.SearchVectorField — Method
SearchVectorField(; kwargs...)

A stored full-text document — PostgreSQL's native tsvector, Django's SearchVectorField. It holds what SearchVector(…) computes, so a search reads the stored document instead of parsing the text of every row on every query.

Fill it from text columns with update and a SearchVector, and search it with the @search lookup, which on this column renders col @@ query with no to_tsvector:

M.Race.objects.filter("year" => 2009).update("search_vector" => SearchVector("name"; config = "english"))
M.Race.objects.filter("search_vector__@search" => SearchQuery("grand prix"; config = "english"))
M.Race.objects.values("name", "rank" => SearchRank("search_vector", SearchQuery("monaco"; config = "english")))

Filled that way, the column does not refresh itself: re-run the update after the text changes. Declare it with generated_from instead and PostgreSQL keeps it current, as a generated column (GENERATED ALWAYS AS (…) STORED) computed from the named columns on every insert and update:

search_vector = Models.SearchVectorField(generated_from = ("title", "body"), config = "simple",
                                         weights = ("A", "B"))

A query's config must be the one the document was built with, or a stemmed word will not match. A GIN index on the column (Models.Index(fields = ("search_vector",), method = "gin", name = …)) serves @search.

It reads as the tsvector's text, a String ('grand':2 'monaco':1 'prix':3), and a String it is given is that text, parsed by PostgreSQL as a tsvector literal. A pattern lookup (@contains, …) on it raises FilterError: search it with @search. SearchVector, SearchHeadline and the to_tsvector the lookup puts around a text column do not take it, since it is already a document.

PostgreSQL only: on SQLite, rendering the column raises BackendCapabilityError. A generated one is created by schema management (PostgreSQL 12 or newer), and PormG writes nothing to it: naming it in create, update, save or a bulk write raises InvalidValueError, and leaving it out lets PostgreSQL compute it. Turning a generated column back into a plain one (removing generated_from, PostgreSQL 13 or newer) keeps its data. Turning a plain column into a generated one, or changing what it is generated from, drops and re-creates the column and its indexes, which migrate runs only with destructive = true.

Keyword Arguments

  • verbose_name::Union{String, Nothing} = nothing: A human-readable name for the field
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values. Usually true: a new row has no document until the update fills it
  • db_index::Bool = false: A plain b-tree index, which no full-text search uses. Declare a GIN Models.Index instead
  • default: not accepted — raises FieldValidationError. A document literal is stored in the form PostgreSQL rewrites it to, so a declared one would never compare equal to the column. Every row's document comes from its own text, through update: declare the column null = true and fill it
  • db_default::Union{NamedTuple, Nothing} = nothing: accepted as on every field (#496), but not a way to give the column a document. A literal one (''::tsvector) raises FieldValidationError: it reads back from the catalog as a value rather than the expression declared, so makemigrations would plan it again each time (#1033)
  • editable::Bool = false: Whether the field should be editable in forms
  • generated_from::Union{Tuple, Vector, Nothing} = nothing: the database column names (a field's db_column where it sets one) a generated column is computed from, in order. Each is cast to text and COALESCEd, so a NULL one adds nothing. They must be other columns of the same model, and not generated ones (ModelDefinitionError, when the model is defined)
  • config::Union{String, Nothing} = nothing: the text-search config of a generated column, such as "english". Required with generated_from: PostgreSQL computes a generated column only from an expression that does not depend on a server setting
  • weights::Union{Tuple, Vector, Nothing} = nothing: one label from "A" to "D" per column of generated_from, as SearchVector's weight. Without it, the columns form one unweighted document

A generated_from that is empty, names a column twice or names something that is not an identifier, a missing or invalid config, weights of another length or with another label, config or weights without generated_from, and a db_default beside it raise FieldValidationError.

Database Mapping

  • PostgreSQL Type: tsvector
  • SQLite: none — rendering the column raises BackendCapabilityError (see above)

Examples

Race::Models.ModelType = Models.Model("race",
  raceid = Models.IDField(),
  name   = Models.CharField(max_length = 255),
  search_vector = Models.SearchVectorField(null = true),
  indexes = [Models.Index(fields = ("search_vector",), method = "gin", name = "race_search_vector_gin")],
)

# Generated: PostgreSQL computes the document from `name` on every write.
Race::Models.ModelType = Models.Model("race",
  raceid = Models.IDField(),
  name   = Models.CharField(max_length = 255),
  search_vector = Models.SearchVectorField(generated_from = ("name",), config = "english"),
  indexes = [Models.Index(fields = ("search_vector",), method = "gin", name = "race_search_vector_gin")],
)

See also search_vector_expression, which indexes a document computed at query time instead.

source
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 field
  • max_length::Int = 50: Maximum number of characters allowed
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = true: Whether to create a database index on this field (true by default for slugs)
  • default::Union{String, Nothing} = nothing: Default slug value
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::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.ModelType = Models.Model(
    id = IDField(),
    name = CharField(max_length=200),
    slug = SlugField(unique=true, verbose_name="URL Slug")
)
source
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: nothing
  • unique::Bool: If true, ensures field values are unique across the table. Default: false
  • blank::Bool: If true, allows empty values in forms/validation. Default: false
  • null::Bool: If true, allows NULL values in the database. Default: false
  • db_index::Bool: If true, creates a database index for faster queries. Default: false
  • default::Union{String, Nothing}: Default text content. Default: nothing
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool: If true, 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"
)
source
PormG.Models.TimeField — Method
TimeField(; kwargs...)

Time of day with no date component — SQL TIME.

default accepts a Time, an ISO time string (e.g. "09:30:00"), a DateTime (its time of day) or a time period such as Hour(5); an invalid value raises FieldValidationError at model-definition time rather than on the first insert. A Bool or a bare number is refused (#885): Time(5) reads it as an hour, so default = 5 was 05:00 and default = true was 01:00.

Examples

Team_store::Models.ModelType = 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.

source
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 field
  • max_length::Int = 200: Maximum number of characters allowed
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{String, Nothing} = nothing: Default URL value
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::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.ModelType = Models.Model(
    id = IDField(),
    name = CharField(max_length=200),
    wiki_url = URLField(null=true, blank=true, verbose_name="Wikipedia Link")
)
source
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 field
  • primary_key::Bool = false: Whether this field is the primary key for the table
  • unique::Bool = false: Whether values in this field must be unique across all records
  • blank::Bool = false: Whether the field can be left blank in forms
  • null::Bool = false: Whether the database column can store NULL values
  • db_index::Bool = false: Whether to create a database index on this field
  • default::Union{String, Nothing} = nothing: Default UUID value as a string
  • db_default::Union{String, NamedTuple, Nothing} = nothing: A database-side expression default, rendered verbatim into the DDL (#496). "CURRENT_TIMESTAMP" and "CURRENT_DATE" render on every engine; any other expression must name its engine — (postgres = "now()",), with sqlite and mysql keys beside it — and raises BackendCapabilityError on an engine it does not name rather than emitting DDL that engine would reject. Mutually exclusive with default. Full rules: the Column defaults section of the Schema Conventions guide
  • editable::Bool = true: Whether the field should be editable in forms
  • auto_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.ModelType = Models.Model(
    id = IDField(),
    session_token = UUIDField(unique=true, db_index=true),
    user_id = ForeignKey("User")
)
source
PormG.Models.bind! — Method
bind!(model, key::String) -> model

Bind one model to the loaded connection key, and do nothing else (#1230): the narrow half of set_models, for a model built at runtime — a library's own infrastructure table, say — that is not part of a models module. It sets the connection the model's queries use and returns the model.

LiveTiming::PormG.Models.ModelType = PormG.Models.Model("live_timing", id = PormG.Models.IDField(),
                                      payload = PormG.Models.TextField())
PormG.Models.bind!(LiveTiming, "db")
PormG.Migrations.ensure_table!(LiveTiming, "db")

key is accepted when it names a configuration that is registered — by load, register_connection or register_offline — or one the connection resolver (Configuration.set_connection_resolver) serves: such a key is resolved here as get_settings(key) would resolve it, so no prior call is needed (#1255). Any other key raises InvalidConfigurationError naming the load call, and no folder is loaded for you (#1226). Unlike set_models, it does not register a module, record a binding, name the model, resolve ForeignKey targets given by name or install reverse accessors — a model that relates to others belongs in a models module registered with @import_models.

source
PormG.Models.connection_key — Method
connection_key(model) -> Union{String, Nothing}

The connection key model is bound to — the configuration its queries run against — or nothing while it is not bound yet (#1229). The public way to ask what the connect_key field holds:

PormG.Models.connection_key(M.Driver)   # "db"

It reads the binding as it stands and does not trigger the self-heal; a model that has not been queried yet in a restored session can still report its import-time key while that key is unloaded.

source
PormG.Models.search_vector_expression — Method
search_vector_expression(columns...; config = nothing, weight = nothing, form = :lookup) -> String

The to_tsvector(…) text a full-text query renders, for a GIN index to be declared on (PostgreSQL only). PostgreSQL uses an expression index only when the query's expression is the index's, so write the index with this rather than by hand: the text comes from the same function the query is rendered with, and a config the query would refuse is refused here.

QueryIndex expression
"surname__@search" => SearchQuery(…; config = "simple")search_vector_expression("surname"; config = "simple")
"doc__@search" on "doc" => SearchVector("forename", "surname"; config = "simple")search_vector_expression("forename", "surname"; config = "simple")
the same, with SearchVector(…; weight = "A")search_vector_expression(…; config = "simple", weight = "A")
"doc__@search" on "doc" => SearchVector("surname"; config = "simple")search_vector_expression("surname"; config = "simple", form = :vector)

One column without a weight has two expressions, and form names the query the index serves: :lookup (the default) is the @search lookup's, to_tsvector('simple'::regconfig, "surname"), and :vector is a SearchVector's, to_tsvector('simple'::regconfig, COALESCE(("surname")::text, '')). They differ on NULL: the lookup's is NULL for a NULL column, SearchVector's is an empty document. Several columns, or a weight, have only SearchVector's expression, so both forms give it: each column cast to text, COALESCEd and joined by a space, inside setweight(…, 'A') when there is a weight. The config must be the query's: SearchQuery's, for the lookup. It is required: a query with no config uses the server's default_text_search_config, and PostgreSQL only indexes the two-argument to_tsvector, so there is no index to declare for it.

columns are database column names (a field's db_column where it sets one), written bare; each is quoted in the result. A column that is not an identifier, no config, or a form other than :lookup or :vector raises ModelDefinitionError, and a config that is not a name or a weight other than "A" to "D" raises InvalidValueError.

Driver::Models.ModelType = Models.Model("driver",
  driverid = Models.IDField(),
  forename = Models.CharField(max_length = 255),
  surname  = Models.CharField(max_length = 255),
  indexes = [
    Models.Index(expressions = (Models.search_vector_expression("surname"; config = "simple"),),
                 method = "gin", name = "driver_surname_tsv"),
  ],
)
source
PormG.Models.set_models — Method
set_models(_module::Module, path::String; implicit = isinteractive(), source = nothing) -> nothing

Register every model _module declares (its Models.@models … line, #1602) against the database configuration folder path, and resolve the relationships between them. A module that declares no model set raises ModelDefinitionError naming the line to add: PormG does not scan a module's names for models. @import_models and @models_module declare it for you. A package app calls it from its __init__, after loading its configuration, on a models module it included in the module body — the run-time bind, which reads no connection.yml at precompile time (see docs/src/configuration/setup.md). In a REPL or a script, PormG.@import_models (or PormG.@models_module for inline definitions) calls it for you.

source is the path of the file that defines _module. With Revise loaded, PormG registers a reload for it: after an edit to that file, the next Revise.revise() (before every REPL command) re-evaluates the module body into the same module and calls set_models again. Without Revise it does nothing, so the same boot runs in production (#1487). @import_models passes it for you.

include("../db/models.jl")    # module body: defines `module models … end`, reads no configuration
function __init__()
    PormG.Configuration.load("db"; root = APP_ROOT, env = get(ENV, "RACECONTROL_ENV", "dev"))
    PormG.Models.set_models(models, "db"; source = joinpath(APP_ROOT, "db", "models.jl"))
end

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:

  1. Names the unnamed. A model declared without a positional table name carries name == ""; it is filled in here from its declared binding, lowercased (Race = Model(...) → table race).

  2. Binds the connection. path is matched against the loaded configurations to find the connection key, strongest match first and preferring an explicitly loaded key over one minted by the implicit load (_resolve_connect_key). A folder that matches no loaded configuration raises InvalidConfigurationError (#1226), naming the folder, the keys that are loaded and the PormG.Configuration.load(…; root, env) call to add before registering the models. PormG does not load the folder itself outside an interactive session (#1248): in a REPL (isinteractive()) it loads it, with a warning, guessing the environment (from PORMG_ENV, else default_env:) and the key (the caller's own path); a service, a test or a script gets that only per call, with implicit = true. Either way it raises MissingConfigurationError if path holds no connection.yml. See docs/src/configuration/advanced.md.

  3. Resolves relationships. Each ForeignKey/OneToOneField target is resolved (a target given as a model-name String is replaced by the model object), pk_field defaults are applied, and the reverse accessor is installed on the target. An omitted related_name is 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 — counting ForeignKey, OneToOneField and ManyToManyField together (#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 raises ModelDefinitionError. ManyToManyFields get their join-table metadata built and cached.

  4. Rejects contradictions. on_delete = SET_NULL on a null = false field, or SET_DEFAULT with no default, raises ModelDefinitionError here — at declaration, rather than later as a mangled UPDATE. 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 duplicate related_name, a model without exactly one primary key, an unusable explicit through model — 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__ an @models_module block defines catches — though the first load of the macro still surfaces the error. The Revise reload catches too, and logs the error with a warning naming the models file. 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.ModelType = Models.Model(
  circuitid = Models.IDField(),
  name      = Models.CharField(max_length = 100),
)

Race::Models.ModelType = 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
end

with the configuration loaded first, e.g. PormG.Configuration.load("db"; env = "dev").

Each model is bound with a typed global, Race::Models.ModelType = …, so M.Race is a typed read rather than a dynamic one, and a reload is an ordinary assignment that reaches a running task too (#1650, #1663). A module that binds its models with plain globals is accepted and registered the same way, with one warning, once per session, naming the first plain binding (#1664). A const binding is typed as well and is not warned about.

A connection with no folder — one built in code with register_connection — is bound by key instead: set_models(_module; key = "f1") (#1606).

See also Model, PormG.@import_models, PormG.@models_module.

source
PormG.Models.set_models — Method
set_models(_module::Module; key::String, source = nothing) -> nothing
set_models(set::ModelSet; key::String, source = nothing) -> nothing

Register every model _module declares against the connection key, with no folder resolution (#1606, proposal 1 of #1599). key is any registered connection — Configuration.load, register_connection (a connection built in code, with no connection.yml), register_offline — or one the connection resolver serves. An unknown key raises InvalidConfigurationError naming it; nothing is loaded for you.

PormG.Configuration.register_connection("f1", "db/f1.sqlite"; adapter = "SQLite", env = "prod")
PormG.Models.set_models(models; key = "f1")

Everything else is what the folder form does: the model set is read (Models.@models), names are filled, relations are resolved and checked, reverse accessors installed, and the key is recorded in the module's model set, from which a restored image rebinds. It is the form for an application that builds its configuration in code — a juliac --trim binary reads no YAML.

A trimmed binary passes the set itself, set_models(models.MODEL_SET; key = "f1"): its image keeps only the module bindings its code references, so PormG cannot read MODEL_SET by name there. source registers the Revise reload, as for the folder form. A connection from register_connection is read-only until its write flags are set.

source
PormG.Models.@models — Macro
@models Circuit Race …

Declare the models a models module defines, by their bindings, as the module's last line, after the models: it binds the module's MODEL_SET, a ModelSet holding those models. Registration (set_models, @import_models) reads it instead of scanning the module (#1602):

module models
import PormG.Models

Circuit::Models.ModelType = Models.Model(circuitid = Models.IDField(), name = Models.CharField())
Race::Models.ModelType    = Models.Model(raceid = Models.IDField(),
                                         circuitid = Models.ForeignKey(Circuit, on_delete = "CASCADE"))

Models.@models Circuit Race
end

The Generator writes this line. @import_models and @models_module add it for you, from the file's top-level X = Models.Model(…) assignments, when the module does not declare it; a module included with plain include and registered with set_models must declare it, or registration raises ModelDefinitionError naming the line to add. List a model the module reaches through using or import to register it here. A long list may be written Models.@models(Circuit, Race, …) across lines.

The line captures the models the bindings hold when it runs, so it comes after their definitions; a model rebound by hand afterwards needs the line run again (the Revise reload re-evaluates the whole module body, so it does). Several @models lines in one module, or the line run again, merge: each adds its models to the module's set, and a binding listed again takes the value it holds then. A model leaves the set only through PormG's reload of the file (set_models(…; source) with Revise loaded, which re-evaluates the whole body) or a fresh include of it; Revise re-evaluating one changed line of a package's own file merges, so it only adds. A juliac --trim application registers with Models.set_models(models.MODEL_SET; key) (#1606).

source
PormG.Migrations.LOSSY_ALTER_KINDS — Constant
LOSSY_ALTER_KINDS

Every kind a LossyAlter can carry, with its class — the closed set, in one place:

kindengineclasswhen
:set_not_nullevery engine:rowsa nullable column becomes NOT NULL
:varchar_lengthPostgreSQL, MySQL:rowsa column becomes a VARCHAR shorter than it could hold
:integer_rangePostgreSQL, MySQL:rowsan integer narrows, or a float/decimal becomes an integer
:decimal_precisionPostgreSQL, MySQL:rowsa NUMERIC loses whole digits (or rounds up into one)
:non_negative_checkevery engine:rowsa >= 0 CHECK is added (e.g. PositiveIntegerField)
:byte_length_checkevery engine:rowsa BinaryField byte bound is added or lowered
:add_not_nullevery engine:rowsa new NOT NULL column with no default is added to a table with rows
:add_uniqueevery engine:rowsunique = true is added to a column holding duplicates
:add_primary_keyevery engine:rowsprimary_key = true moves to a column holding duplicates (or NULLs, on PostgreSQL and MySQL)
:add_composite_uniqueevery engine:rowsa UniqueConstraint is added over columns holding duplicate tuples
:add_checkevery engine:rowsa CheckConstraint is added that some rows fail
:add_foreign_keyevery engine:rowsa foreign key is added or re-pointed over rows with no parent
:decimal_scalePostgreSQL, MySQL:silenta NUMERIC scale is lowered, so values round
:to_integerPostgreSQL, MySQL:silenta float/decimal becomes an integer, so values round
:to_datePostgreSQL, MySQL:silenta timestamp becomes a date, dropping the time
:to_timePostgreSQL, MySQL:silenta timestamp becomes a time, dropping the date
:drop_timezonePostgreSQL:silenttimestamptz becomes timestamp, dropping the offset
:text_affinitySQLite:silenttext becomes a numeric/boolean column, so '0042' stores as 42
:text_castPostgreSQL, MySQL:rowstext becomes a number, boolean, date, timestamp, UUID, JSON, an IP address or network, or an array (the plan casts with USING) — or an array's element type changes other than by widening (it converts through text) — and some values do not parse
:host_bitsPostgreSQL:rowsan IP address (inet) becomes a network (cidr), and some values have bits set right of their mask (10.0.0.1/24)
:to_booleanPostgreSQL, MySQL:silenta number becomes a boolean (USING "c" <> 0), so every non-zero value becomes true
:drop_defaultPostgreSQL, MySQL:silenta USING retype (on MySQL, any retype) must drop a database default the model does not declare, and nothing puts it back
:no_implicit_castPostgreSQL, MySQL:refusedthe engine has no automatic cast between the two types and the plan writes no USING — since #28, an array becoming a scalar other than text, or a scalar other than text becoming an array; on MySQL, a pair PostgreSQL has no cast for that MySQL converts by a rule changing every value (a date into a time or a number, a time into a date, a number into a date or a UUID, a boolean into JSON) or only as text (binary, JSON, a UUID)

:rows findings are counted before migrate runs and refuse the plan when any row would fail; :silent ones need destructive = true; :refused ones cannot be applied as planned.

Since #828 every pair :no_implicit_cast used to name gets a USING (see Dialect._postgres_retype_using), so the planner no longer records it: text into a parsed type is :text_cast, a number into a boolean :to_boolean, and a boolean into a number nothing at all — true is 1 and false 0. The kind stays, for a plan written before #828, whose SQL has no USING and which is still refused — and #28 gives it live pairs again: an ArrayField and a scalar column other than text have no conversion PormG would write.

source
PormG.Migrations.MIGRATION_FORMAT_VERSION — Constant
MIGRATION_FORMAT_VERSION

Version 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.

source
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).

source
PormG.Migrations.DryRunResult — Type
DryRunResult

Result 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.

  • checksum, statements — the plan's SQL in execution order, and its digest.
  • destructive_statements — the statements the destructive guard flags (a DROP, a TRUNCATE, a DELETE with no WHERE).
  • lossy_alters — the plan's lossy column changes (#803), one LossyAlter each, with rows counted against the live database for those that fail on existing rows. One marked handled=pre (#897) is counted too, but migrate does not refuse the plan for it.
  • data_steps — the labels of the plan's hand-written data steps (#740), in the order they run: every Data (pre): … before the schema statements, every Data (post): … after them.
source
PormG.Migrations.LossyAlter — Type
LossyAlter

One column change in a migration plan that can fail on, or silently change, rows already in the table (#803). makemigrations records each one from the column's ColumnDelta and writes it into the plan's header; dry_run lists them and migrate acts on them.

  • kind — a key of LOSSY_ALTER_KINDS, e.g. :set_not_null, :varchar_length, :decimal_scale.
  • table, column — the names the live catalog knows before the plan runs, so a renamed column is reported under its old name. For a table-level constraint (:add_composite_unique, :add_check) column holds the constraint's name.
  • old_type, new_type — the two column types as rendered; old_type is empty for a column the plan adds (:add_not_null).
  • bound, scale — the limit the pre-check compares against (a VARCHAR length, an integer width in bits, whole digits and scale, a byte bound), or nothing.
  • rows — how many existing rows the change would fail on, filled in by dry_run / migrate for the :rows class; nothing until counted, and always nothing for the other classes. A finding the pre-check cannot count against the live schema (a foreign key whose parent table this same plan creates or renames) also stays nothing, and the database checks it when the migration runs.
  • columns — the catalog's columns of a composite UniqueConstraint (:add_composite_unique); empty otherwise.
  • references — (table, column) of the parent a foreign key points at (:add_foreign_key), or nothing.
  • condition — the SQL condition of a CheckConstraint (:add_check), the WHERE of a partial UniqueConstraint (:add_composite_unique, #934), or nothing.
  • handled — :pre when the operator marked the finding handled=pre in the plan's header (#897): a Data (pre): step of the same plan fixes the rows, so migrate still counts them but does not refuse the plan for them, and the database checks the change when the migration runs. nothing otherwise — makemigrations never writes it.
source
PormG.Migrations.MigrationPrecheckError — Type
MigrationPrecheckError(msg, findings)

Raised by migrate when the pending plan changes a column or adds a constraint in a way existing rows cannot survive — a SET NOT NULL over rows that hold NULL, a VARCHAR(n) shorter than values already stored, an integer too narrow for them, duplicates under a new UNIQUE, rows with no parent under a new foreign key — or in a way PostgreSQL cannot apply at all. findings lists each LossyAlter, with rows counted.

Nothing has been written when it is raised: the pre-check counts before the migration starts. destructive = true does not bypass it, because no opt-in can make those rows fit. Fix the data (or the models file) and run makemigrations() again; a hand-written backfill or USING added to the plan is the other way through, described in the migrations workflow guide.

Like DestructiveMigrationError, it is raised only where nobody can be asked: at an interactive terminal the same plan logs the findings and migrate returns :declined.

source
PormG.Migrations.MigrationResult — Type
MigrationResult

What one migrate call did. Every path through migrate that does not throw returns one, so a script that runs migrate at boot can branch on the outcome instead of parsing the log.

  • outcome — one of:

    outcomeMeaning
    :appliedThe pending plan ran in one transaction and is recorded in pormg_migrations.
    :already_appliedThe pending plan is the latest applied migration (its checksum matches): another instance applied it first, or a previous migrate committed it and then failed to archive the file. Nothing ran; the file is archived — by this call, or already by an instance sharing the plan folder (#81).
    :nothing_pendingThere is no pending_migrations.jl, or it holds no statements. The history table and any configured extensions were still ensured.
    :disabledThe connection is change_db: false. Nothing was read or written.
    :declinedAn interactive run was not confirmed at the prompt, a destructive plan was refused there for lack of destructive = true, or a column change would fail on existing rows (#803).
  • version — the pormg_migrations.version of the row involved: the new row for :applied, the matched row for :already_applied, nothing for every other outcome.

  • n_statements — how many plan statements this call executed: the plan's size for :applied, 0 for every other outcome.

A failure is still an exception, not an outcome: a destructive plan without destructive = true in a non-interactive run (DestructiveMigrationError), a column change that would fail on existing rows (MigrationPrecheckError, raised before anything is written), a database that no longer holds the schema the plan was generated against (PlanPreconditionError, raised before anything is written, #739), a plan file that does not parse (InvalidMigrationError), a statement the database rejects (the plan is rolled back, recorded as failed, and the error rethrown), and a migration lock not acquired within lock_wait (OperationalError).

result = PormG.Migrations.migrate("db"; interactive = false)
if result.outcome === :applied
    @info "Schema migrated" result.version result.n_statements
end

See the Deploying guide for running migrate at application boot.

source
PormG.Migrations.MigrationStatus — Type
MigrationStatus

Structured result from status(). Contains applied migrations, pending files, failed migrations, and drift signals about the migration history (see status).

A failed row whose plan a later run applied — a later applied row with the same checksum — is in superseded, not in failed (#739): on PostgreSQL and SQLite a plan runs in one transaction, so the failure wrote nothing, and the retry that succeeded resolved it. failed holds only the failures nothing has resolved.

source
PormG.Migrations.PlanPreconditionError — Type
PlanPreconditionError(msg, tables)

Raised by migrate when the database no longer holds the schema the pending plan was generated against (#739). makemigrations records a fingerprint of every table its diff compared in the plan header; migrate reads the same tables again and refuses on any difference — once before the migration lock, so a stale plan fails before any prompt or row count, and again inside it, which is the check that decides. Either way no plan statement runs and no failed row is recorded; a refusal inside the lock comes after migrate has ensured the history table and the configured extensions, as it does on every call.

tables lists each table that differs, as (table, expected, found) fingerprints, where "absent" means the table does not exist. A table the plan never compared — one created in the database after makemigrations, outside the models — is not part of the precondition.

This is what stops an old plan from replaying: a pending_migrations.jl baked into an earlier release names the schema before its own changes, which a database already past them no longer holds. To apply a plan anyway, regenerate it against this database with makemigrations(), or delete its # pormg-schema-table: lines: a plan without them applies without the check.

source
PormG.Migrations.SchemaCheckFinding — Type
SchemaCheckFinding

One fact about the live database, as reported by check.

  • kind — the finding class: :expression_default, :schema_drift or :invalid_index.
  • table — the table name.
  • columns — the column name(s) the finding is about. One entry for a column finding, none for a table-level one (a :schema_drift "New model" or "Drop table", and every :invalid_index).
  • detail — the text the finding is about, verbatim: for :expression_default, the DEFAULT expression as the database renders it; for :schema_drift, the label of the step makemigrations would plan ("Add field: country", "Drop table", …); for :invalid_index, the index name.
  • message — a one-line explanation of the consequence (for :invalid_index, also the remedy and the index definition).
source
PormG.Migrations.SchemaCheckResult — Type
SchemaCheckResult

Structured result from check: the backend that was read (:postgres or :sqlite) and the SchemaCheckFindings, ordered by kind, then table, then column so two runs against the same schema render identically.

isempty(result) is true when the requested finding classes found nothing — for kinds = [:schema_drift], when the database matches the declared models. That is the CI gate:

r = PormG.Migrations.check("db"; kinds = [:schema_drift])
exit(isempty(r) ? 0 : 1)
source
PormG.Migrations.check — Method
check(connection, settings; kinds = [:expression_default], ignore_table = nothing, include_table = nothing, models_file = nothing) -> SchemaCheckResult
check(settings; kwargs...) -> SchemaCheckResult
check(db::String; config = config, kwargs...) -> SchemaCheckResult

Report facts about the live database schema, one SchemaCheckFinding each. Read-only on both backends: check never writes, never prompts, and does not consult change_db, so it runs against a production connection.

kinds selects the finding classes:

  • :expression_default (the default) — columns whose DEFAULT PormG's models express only as a db_default. Needs no models file, no migration history and no init_migrations().

  • :schema_drift — every place the database differs from the declared models: one finding per step the next makemigrations would plan. Empty means the database matches the models, which makes it a CI or release gate:

    r = PormG.Migrations.check("db"; kinds = [:schema_drift])
    exit(isempty(r) ? 0 : 1)
  • :invalid_index — every index PostgreSQL has marked invalid, typically left by a failed CREATE INDEX CONCURRENTLY. PostgreSQL-only in effect: SQLite has no such state, so there the class is accepted and always reports nothing.

ignore_table replaces the backend's default skip list (postgres_ignore_table / sqlite_ignore_schema, less the connection's unignore_defaults: entries, #818); tables registered through register_ignore_tables! and the connection's own ignore_tables: in connection.yml (#749) are always skipped on top of it, so check reads exactly the tables the importer and makemigrations do. include_table restricts the read to the named tables. Both match the parameters of convert_schema_to_models. For :schema_drift the two filter differently, on purpose:

  • include_table restricts what is reported. Every declared model is still planned, so the findings for a listed table are exactly the steps makemigrations would plan for it. A ManyToManyField's through table is reported only when it is listed too.
  • ignore_table skips live tables only, as the default skip list does for makemigrations. A declared model on a table only this keyword names therefore reads as missing from the database. A managed model on a table under one of the lists makemigrations reads with, which are the backend default, register_ignore_tables! and the connection's ignore_tables:, raises InvalidConfigurationError instead, as it does in makemigrations (#749, #805). Replacing the default list with this keyword does not switch that refusal off; listing the default entry under unignore_defaults: does, for that connection (#818).

models_file names the models file :schema_drift compares against; by default it is settings.model_file under the connection's folder, the file makemigrations reads. An empty kinds, an unknown class, or a models_file without :schema_drift raises InvalidValueError.

Run it alongside status and dry_run in the operator flow, and before upgrading PormG.

:schema_drift

Each finding is one planned step: table is the table, columns the column a field step names, detail the step's label exactly as makemigrations writes it into a plan ("New model", "Drop table", "Add field: country", "Remove field: code", "Alter field: points", …), and message which side has what the other lacks.

  • It reads what makemigrations reads, and plans the same way — the same live reader, the same models loader, the same planner, with interactive = false. So it cannot disagree with the next makemigrations about whether there is a change.
  • A failed read raises, as it does in makemigrations: a gate must never report "clean" because it could not look.
  • An unhinted rename is drift. With no one to ask, a renamed column plans as an add plus a remove, and a renamed table as a new model plus a drop. Both findings are reported, and each message names the other half it could pair with.
  • A changed column reads differently per engine. PostgreSQL alters a column in place and reports "Alter field: <column>". SQLite rebuilds the table, so the finding is "Alter table: <table>" and names no column. Added and removed columns keep their "Add field: …" / "Remove field: …" labels on both engines.
  • The declared models are loaded the way makemigrations loads them — the file is included into a throwaway module and set_models never runs — so checking does not touch the models an application has already loaded.
julia> PormG.Migrations.check("db"; kinds = [:schema_drift])
Schema Check (postgres):
  2 finding(s)

  schema_drift (2)
    ⚠ circuit.country  Add field: country — declared in the models, missing from the database
    ⚠ season  New model — declared in the models, missing from the database

:expression_default

A column whose DEFAULT is a SQL expression (now(), CURRENT_TIMESTAMP, gen_random_uuid(), concat(...)) rather than a literal value. Since #496 PormG can express one: the column imports as db_default= carrying exactly the text shown, so detail is the value to paste into your model. Two things are worth knowing about such a column, and they are why the class still earns its place:

  • declaring it as default= instead is the one response that causes damage — that makes makemigrations propose SET DEFAULT '<the expression>', a quoted literal written over the database's real expression default, after which every new row stores that text;
  • unless the expression is one of the portable ones (CURRENT_TIMESTAMP, CURRENT_DATE) it is pinned to this engine. A models file carrying it renders here and raises a BackendCapabilityError on the other backend, which is deliberate — PormG will not guess a translation — but it means a portable app needs db_default = (postgres = …, sqlite = …) spelled out.

Re-scoped rather than retired (#496). The detected set is unchanged; what changed is that the finding now says how to describe the column rather than that it cannot be described.

A primary key that imports as an IDField is deliberately not reported — a serial/bigserial id column, say. sIDField has neither a default that could hold an expression (Union{Int64, Nothing}) nor a db_default slot at all: PostgreSQL rejects a column that is both GENERATED … AS IDENTITY and carries a DEFAULT, so #496 could not give it one. There is therefore nothing a model could declare there, and reporting it would be advice that cannot be followed. (That a pre-existing serial key is not byte-identical to what PormG would emit is a separate matter, and makemigrations already proposes the IDENTITY alteration for it.)

That skip is wider than "an integer key", and the width is the point. Any key on the fall-through arm compiles to an IDField — a BLOB, a REAL, a numeric — with the sole exception of a lengthless TEXT key on SQLite, which becomes UUIDField(primary_key = true). check asks _arm_carries_db_default, the same predicate the importer's arm selection implies, so the two cannot disagree about it. Before #496 widened it the skip was integer-only, which was correct for the OLD message ("declare no default= here", true of every key) and wrong for the new one: a BLOB PRIMARY KEY DEFAULT (randomblob(16)) was reported and told to declare a db_default= that IDField warns about and ignores. Found in review.

A key that imports as a UUIDField, a CharField or a relation IS reported, because those arms carry the expression and their spelling is worth showing.

This is the diagnostic a warning cannot be. Since #496 the importer emits no warning for these columns — there is nothing being lost to warn about — so check is the only way to ask the question without reading a generated models file, and the only one that works before you have generated one.

julia> PormG.Migrations.check("db_2")
Schema Check (postgres):
  2 finding(s)

  expression_default (2)
    ⚠ lap_note.created_at  DEFAULT now()
    ⚠ lap_note.note  DEFAULT concat('a'::text, 'b'::text)
      the DEFAULT is a SQL expression; it imports as `db_default=` with exactly this text. Declare it that way and NOT as `default=`, which would render it as a quoted literal

:invalid_index

An index whose pg_index.indisvalid is false. The usual source is a CREATE INDEX CONCURRENTLY or REINDEX CONCURRENTLY that failed part way (a duplicate value under a UNIQUE build, a deadlock, a cancelled statement), often inside a run_once step with transaction = false. PostgreSQL never uses such an index for queries, yet it still costs on writes, and a unique one may still reject duplicates. An index being built CONCURRENTLY right now reads as invalid too, until the build finishes.

One finding per index, on any table the other classes read, whatever its shape: table is the table, columns is empty, detail the index name, and message the consequence, the remedy and the index definition. The remedy always leads with DROP INDEX CONCURRENTLY, quoted and schema-qualified so it pastes as is. A REINDEX INDEX CONCURRENTLY alternative (PostgreSQL 12 or later) is offered as something to run once the cause is gone — a unique build that failed on duplicates fails the same way again — and never for a <name>_ccnew / <name>_ccold copy a failed REINDEX CONCURRENTLY left behind: drop that copy and do not re-create it, since the original (or the rebuilt index) is already in place, and reindexing it would make a valid duplicate.

It reports; it never acts. makemigrations neither reads nor drops an invalid index (#847): a field declaring db_index = true over a column carrying only an invalid index gets PormG's own index beside it, and the invalid one stays until you drop it. Rebuilding or dropping it is your decision, because only you know whether the failed build is still wanted.

julia> PormG.Migrations.check("db_2"; kinds = [:invalid_index])
Schema Check (postgres):
  1 finding(s)

  invalid_index (1)
    ⚠ driver  driver_code_uq — invalid index: PostgreSQL never uses it for queries, but it still takes disk space and keeps updates to its columns from being HOT. One still being built CONCURRENTLY reads the same until it finishes. Usually a failed CREATE INDEX CONCURRENTLY. Drop it with `DROP INDEX CONCURRENTLY "public"."driver_code_uq";`, remove what made the build fail (for a unique index, usually duplicate values), and create it again from the definition below; on PostgreSQL 12 or later, `REINDEX INDEX CONCURRENTLY "public"."driver_code_uq";` rebuilds it in place instead, once the cause is gone. Definition: CREATE UNIQUE INDEX driver_code_uq ON public.driver USING btree (code)

See also SchemaCheckResult, SchemaCheckFinding.

source
PormG.Migrations.convertSQLToModel — Method
convertSQLToModel(row; conn = _PostgresEngine()) -> PormGModel

The model inspectdb writes for one row of get_database_schema(::PormGPostgres): _pg_live_table compiled through model_from_live. row is any Tables.jl row with the dump's columns: the NamedTuple the dump holds, or a DataFrameRow of a caller's own frame. conn only picks the engine's struct choices and touches no connection.

source
PormG.Migrations.convertSQLToModel — Method
convertSQLToModel(db::PormGSQLite, table_name) -> PormGModel

The model inspectdb writes for one live SQLite table: _sqlite_live_table compiled through model_from_live. The table name may be spelled in any case (#531); the model is named by the catalog spelling.

source
PormG.Migrations.convertSQLToModel — Method
convertSQLToModel(sql::String) -> PormGModel

The model for one CREATE TABLE statement, read the way a live table is (#522): the DDL is executed in a throwaway SQLite file and the result goes through the same reader as convertSQLToModel(db, table), so there is no second, regex-driven reader to keep in step with the first — the one this replaced had two documented gaps (unique never read, a non-canonical to_table) for exactly that reason.

The statement is executed as given — one SQL statement, in a scratch database that holds nothing else — so pass only DDL you would run yourself. It must name its table in double quotes, as PormG writes it; an InvalidMigrationError says so otherwise. SQLite does not check that a REFERENCES target exists at CREATE TABLE time, so a statement carrying foreign keys reads fine on its own — and, the parent being absent, its to_table keeps the REFERENCES spelling rather than a canonical one.

A name holding a " is read whole (#837): a quoted identifier escapes it as "", which is how PormG's own DDL writes such a db_table. If the statement does not create the table it names, an InvalidMigrationError says so, rather than an empty model coming back.

source
PormG.Migrations.convert_schema_to_models — Method
convert_schema_to_models(db; ignore_table, include_table) -> Vector{PormGModel}

The models inspectdb writes for the live database: read_live_schema compiled through model_from_live, table by table. makemigrations does not call this any more — it diffs the LiveTables directly — so a struct chosen here is a choice about the generated file, never about the plan.

source
PormG.Migrations.discard_pending_migration — Method
discard_pending_migration(settings; backup=true) -> NamedTuple | Nothing
discard_pending_migration(db::String; config=config, backup=true) -> NamedTuple | Nothing

Discard 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 mark_applied / remove_migration_record, 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 — except for a plan holding hand-written Data (pre): / Data (post): steps, which makemigrations refuses to overwrite (#740). Discarding is how you throw such a plan away on purpose.

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.

source
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_django
source
PormG.Migrations.dry_run — Method
dry_run(connection, settings) -> DryRunResult

Analyze pending migrations without applying them. Validates ordering, checksums, destructive actions, and SQL generation. Does NOT modify the database or move files.

When the plan's header records a lossy column change (#803), dry_run reads the database: it checks each such column still exists and, for a change that can fail on existing rows, counts those rows with a read-only SELECT COUNT(*), reported in r.lossy_alters. A plan with no such change reads nothing.

source
PormG.Migrations.ensure_table! — Method
ensure_table!(model, key::String; indexes = true, force = false) -> Symbol

Create model's table on the connection key if it does not exist yet, and return what happened: :created, :exists, or :disabled when the connection is change_db: false (nothing is read or written, as with migrate). For the infrastructure tables a library ships on top of PormG (a live-timing feed table, say) that are kept out of makemigrations with register_ignore_tables! (#1230).

force = true creates the table on a change_db: false connection too (#1677). change_db: false is the documented production posture and what register_connection builds, and it is migrate's rule for the application's schema; a table register_ignore_tables! keeps out of the migrations is not part of that schema, so the library that owns it says so at the call site. Pass it only for such a table. Everything else is unchanged: the lock, the transaction refusal, and :exists for a table that is already there — force never alters or recreates a table.

The DDL is what a migration would emit for a new model — CREATE TABLE, its foreign keys, indexes (including a PostgreSQL Index(...; opclasses = ("text_pattern_ops",))), unique constraints and CHECKs — rendered for the connection's engine and run in one transaction. indexes = false leaves the plain indexes out. On PostgreSQL it runs under the migration advisory lock, and the existence check is repeated under it, so two instances booting together create the table once; on SQLite the check and the DDL share one write transaction.

PormG.Migrations.ensure_table!(M.Lap_times, "db")   # :created the first time, :exists after
PormG.Migrations.ensure_table!(M.Lap_times, "db"; force = true)   # also on a change_db: false connection

ensure_table! never alters a table that exists: a changed model is migrate's job. A key that is not loaded raises InvalidConfigurationError. Inside an open transaction on that connection it raises TransactionError: its DDL would become a savepoint that commits only with the outer block, after the lock that serializes it is released.

source
PormG.Migrations.get_migration_plan — Method
get_migration_plan(live::Vector{LiveTable}, current_schema, conn, settings; interactive = true, renames = [])
get_migration_plan(models::Vector{PormGModel}, current_schema, conn, settings; interactive = true, renames = [])

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.

The two schema arguments read backwards

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.

The live side is a vector of LiveTables — what read_live_schema returns — and the diff runs on their ColumnSpecs directly (#522): no PormGField is reconstructed from the catalog. The Vector{PormGModel} form reads each model as a live table through live_table; it exists for callers that already hold models (the planner's own tests hand-build the live side that way) and is not what makemigrations uses.

An empty live side 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. Answer yes for a new table, or the number of the table it was renamed from; no asks for that number on its own. A model is not asked when every vanished table has already been claimed by an earlier answer. A field with no matching column is asked the same way, by number or no.

The candidates are ranked by the column IR (#735). A table is listed by how many of the model's columns it holds with the same definition, most first, so (k of n columns match). A field lists the removed columns with the same definition first, and every other one names what renaming it would also change. Nothing is filtered out. The models are asked about in table-name order, and their fields in declaration order. An auto many-to-many join table, and the column named after a renamed end, follow that end's rename without a question when exactly one vanished table or column fits.

renames names renames without a question (#734): "old_table" => "new_table", and "table.old_column" => "table.new_column" with the table's new name. Every name is physical (db_table, db_column). A hint wins over the prompt. A hint whose old name is gone and whose new one exists has already run, and does nothing; one naming neither warns. InvalidMigrationError is raised for a hint that contradicts the schema: both names exist, the old one is still declared, the new one is not declared, or two hints share a name. "old" => nothing says the old one is not a rename: it is dropped and offered to no question.

interactive = false answers "new table" and "not a rename" for every unhinted pair, except a pair with the same definition: a vanished table holding exactly the model's columns (one besides its key, at least), or a removed column identical to the added one. That is the shape of a rename, so the run raises InvalidMigrationError listing every such pair and the hint that decides it, rather than dropping the rows. fail_closed = false turns that off; check(kinds = [:schema_drift]) uses it, because it reports such a pair as drift.

Any answer the prompt does not recognise — an empty line, a typo, an unlisted number — raises InvalidMigrationError, and so does reaching the end of input (#726). The prompts read stdin whenever interactive = true; unlike migrate, nothing checks for a terminal, because answers piped in through a non-terminal stdin are read like typed ones. A script or CI job that has no answers to give passes interactive = false.

A chosen rename plans ALTER TABLE "<old>" RENAME TO "<new>" under the new model's key, plus that table's column changes diffed against the old live table (#615). The rename executes before every column statement (see _order_statements), so those changes name the new table; only the plan-time catalog lookups ask for the old one.

Tables whose foreign key points at the renamed model plan nothing for it (#678): both engines carry the constraint across the rename, so the live references are retargeted to the new name before any table is diffed (see _retarget_references). That is why every table-rename question is asked before any field-rename question. A child that also changes its key — a different on_delete, say — still re-points, against the new name, after the rename has run.

source
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 = "f1_paddock", 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_spectator and Access_spectator, never Spectator and Spectator2. 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 spectator and the other to spectator2. 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_spectator would become spectator while access_spectator survived intact, and the reverse lookup would then want Spectator while the binding is Core_spectator. 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.

source
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 the model.py file 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's db_def_folder unless output_path overrides it, and the table-name prefix comes from that configuration's django_prefix unless django_prefix overrides it. Defaults to DB_PATH.

  • force_replace::Bool: If true, forces replacement of the existing models file. Defaults to false.

  • file::String: The name of the file to save the generated models. Defaults to "automatic_models.jl". Its name without .jl becomes the module's name, so it must be a Julia identifier (f1_models.jl, not f1-models.jl) and not a name the file imports (Models, CASCADE, …); otherwise the call raises ModelDefinitionError and writes no models file (#1486).

  • output_path::Union{Nothing, String}: Directory to write the generated file into, overriding the resolved config's db_def_folder. Use this to stage a foreign Django app's models next to their copied model.py (e.g. "db_garage") while still resolving db for its Settings. Defaults to nothing (use db_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. missing inherits the resolved config's django_prefix; nothing emits unprefixed table names; a String (e.g. "garage") forces <prefix>_<table>. Use this when the imported app uses a different Django app_label than the db config's. Defaults to missing (inherit).

    Since #345 the prefix is written into db_table, not into the positional model name: class Pit_board under django_prefix = "garage" emits Pit_board = Models.Model(db_table = "garage_pit_board", …) — the logical name "pit_board" is the binding lowercased, so it is left for loading to derive (#1043). A Meta.db_table in the Django source still overrides it, exactly as in Django. An empty string is treated as no prefix.

    Each auto-derived ManyToManyField also gets its join table pinned to Django's spelling, which is <the owning model's table>_<field> — so garage_pit_board_lanes normally, but <Meta.db_table>_<field> when the class declares one. PormG's own derivation (<logical model>_<field>) reproduces neither. A field carrying its own db_table, or a through=, is left alone.

  • auth_user_model::Union{Nothing, String}: which model settings.AUTH_USER_MODEL refers to, spelled as Django spells it ("access.User", or a bare "User" when unambiguous). Defaults to nothing, which auto-detects the single class inheriting AbstractUser. If a relation names settings.AUTH_USER_MODEL and there is not exactly one candidate, the import raises InvalidMigrationError naming them — deliberately hard, since one omitted keyword would otherwise turn every user relation in the project into a plain integer column.

  • strict_relations::Bool: when false (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. true raises InvalidMigrationError instead. The lenient default is what makes the importer usable on a project that touches django.contrib.

  • strict_fields::Bool: when false (the default), a field whose Django type PormG does not implement (FilePathField, SmallIntegerField, …) is skipped — the column is not imported, and a @warn plus a # PormG: marker name the field, its class and its models.py line. true raises InvalidMigrationError instead. 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, so makemigrations reads it as drift and proposes dropping it until you declare it by hand — which is the case true is 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_garage/) with its own folder and prefix:

importmodelsfromdjango("dbgarage/models.py"; db="db", file="garagemodels.jl", outputpath="dbgarage", djangoprefix="garage", force_replace=true)

source
PormG.Migrations.import_models_from_mysql — Function
import_models_from_mysql(db::String = "db"; force_replace::Bool = false, ignore_table = nothing, include_table = nothing, include_views::Bool = false, file::String = "automatic_models.jl")

inspectdb for MySQL (#1130 phase 4c): read the tables of the connection db names, a MySQL one, and write a models file for them, as import_models_from_postgres and import_models_from_sqlite do for theirs.

Keyword Arguments

  • force_replace: overwrite an existing models file. Without it an existing file is kept, with a warning.
  • ignore_table: tables to leave out; nothing is the connection's default list (MySQL's own tables, and the connection's ignore_tables:).
  • include_table: read only these tables.
  • include_views: also write each view, as an unmanaged model with no key.
  • file: the file name, in the connection's folder. Its name without .jl becomes the module's name, so it must be a Julia identifier (f1_models.jl, not f1-models.jl) and not a name the file imports (Models, CASCADE, …); otherwise the call raises ModelDefinitionError and writes no models file (#1486).

A column reads as the field whose values it holds: a type MySQL has that PormG does not write as itself (an integer's display width, any datetime(n) or time(n), a timestamp, text) reads as that field, and one whose values differ (an unsigned integer, enum, char(n), float) is emitted as a TextField with a warning, which the next makemigrations retypes. MySQL's re-printed SQL — a CHECK's condition, a functional index's expressions, an expression default — is written without its backticks and introducers; what it cannot be written as (a reserved-word column, a backslash in a literal) is left out, with a warning.

Examples

PormG.Configuration.load("db_my")
PormG.Migrations.import_models_from_mysql("db_my"; include_views = true)
source
PormG.Migrations.import_models_from_postgres — Method
import_models_from_postgres(db::String; force_replace::Bool=false, ignore_table=nothing, include_table=nothing, include_views::Bool=false, 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::Union{Vector{String},Nothing}=nothing: Table name prefixes to ignore. nothing is the connection's default list: postgres_ignore_table, less any unignore_defaults: entry (#818). A vector replaces it. The connection's ignore_tables: and register_ignore_tables! are added either way.
  • include_table::Union{Vector{String},Nothing}=nothing: When set, only these tables are imported.
  • include_views::Bool=false: Also write each view and materialized view as a managed = false model, after the tables (#767). A view has no primary key and none is guessed, so the model is keyless and carries a # PormG: marker saying so. include_table and ignore_table filter views by name too.
  • file::String="automatic_models.jl": The output filename for the generated models. Its name without .jl becomes the module's name, so it must be a Julia identifier (f1_models.jl, not f1-models.jl) and not a name the file imports (Models, CASCADE, …); otherwise the call raises ModelDefinitionError and writes no models file (#1486).

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")
source
PormG.Migrations.import_models_from_sqlite — Function
import_models_from_sqlite(db::String="db"; force_replace::Bool=false, ignore_schema=nothing, include_table=nothing, include_views::Bool=false, 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::Union{Vector{String},Nothing}=nothing: Table name prefixes to ignore. nothing is the connection's default list: sqlite_ignore_schema, less any unignore_defaults: entry. A vector replaces it. The connection's ignore_tables: and register_ignore_tables! are added either way.
  • include_table::Union{Vector{String},Nothing}=nothing: When set, only these tables are imported.
  • include_views::Bool=false: Also write each view as a managed = false model, after the tables (#767). A view has no primary key and none is guessed, so the model is keyless and carries a # PormG: marker saying so. include_table and ignore_schema filter views by name too.
  • file::String="automatic_models.jl": The output filename for the generated models. Its name without .jl becomes the module's name, so it must be a Julia identifier (f1_models.jl, not f1-models.jl) and not a name the file imports (Models, CASCADE, …); otherwise the call raises ModelDefinitionError and writes no models file (#1486).

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")
source
PormG.Migrations.init_migrations — Method
init_migrations(connection::_SQLEngine)

Create the pormgmigrations history table if it does not already exist. migrate(), `markapplied,markfailedandremovemigration_recordcall it themselves; it can also be invoked explicitly for bootstrapping.status()` does not: it is read-only, and reports a missing table instead of creating one.

source
PormG.Migrations.is_destructive — Method
is_destructive(r::DryRunResult) -> Bool

Whether applying the plan needs migrate(…; destructive = true): it has a destructive statement, or a column change that silently alters existing values (a lower NUMERIC scale, say — #803). A plan that would fail on existing rows is not "destructive"; migrate refuses it whatever the opt-in — see r.lossy_alters.

source
PormG.Migrations.is_destructive — Method
is_destructive(sql::String) -> Bool

Whether sql (one statement or several) holds a statement the guard treats as data-losing. That is what migrate refuses to apply non-interactively without destructive = true. It flags:

  • any DROP: a DROP <object> command of any kind (TABLE, INDEX, VIEW, SCHEMA, FUNCTION, SEQUENCE, …), and ALTER TABLE … DROP [COLUMN] x / DROP CONSTRAINT. The exceptions are the ALTER [COLUMN] <col> DROP NOT NULL | DEFAULT | IDENTITY | EXPRESSION sub-clauses, which remove a property rather than data;
  • TRUNCATE, with or without TABLE;
  • DELETE FROM with no WHERE: TRUNCATE by another name, and SQLite's only spelling of it.

UPDATE without WHERE is not flagged: that is the ordinary shape of a backfill.

It reads the SQL text and does not parse it, so it errs toward flagging: a string literal or quoted identifier that reads drop x is reported too. A generated SET DEFAULT 'Drop zone' is one example. That costs an explicit destructive = true, whereas a missed statement would run unasked (#728). Literals are deliberately not stripped first, because that would hide the EXECUTE 'DROP TABLE ' || t inside a DO block. It also misses a few hand-written spellings: any WHERE excuses a DELETE, even WHERE true, and a keyword glued to a quoted name or a comment (DROP"col", DELETE/**/FROM) is not seen.

source
PormG.Migrations.makemigrations — Method
makemigrations(db::String; models_file = nothing, interactive = true, renames = [])
makemigrations(connection, settings::PormGSettings; path = "db/models.jl", interactive = true, renames = [])

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

  • models_file (String form): the models file to diff against. Defaults to <db>/<settings.model_file>; a relative path resolves against the working directory. Name an older models file — one checked out from git, say — and the plan takes the database back to that state: that is how PormG reverts, since it has no rollback. The plan header records a non-default file, and migrate snapshots that file as the applied migration's _old_models.jl. See Reverting by declaring the old state.
  • path (connection form): the models file, as given.
  • interactive: when true, 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. Answer yes, or the number of the old table. An unrecognised answer, or the end of input, raises InvalidMigrationError; no terminal is detected, so a script or CI job with no answers to give must pass false. false asks nothing: an unhinted pair is planned as new, except a pair with the same definition (a likely rename), which raises InvalidMigrationError naming the hint that decides it (#734).
  • renames: the renames to plan without a question, e.g. renames = ["drivers" => "driver", "result.statusid" => "result.racestatusid"] — physical names, a column with its table's new name. "old" => nothing drops the old one instead. A hint that has already run does nothing; one that contradicts the schema raises InvalidMigrationError. See get_migration_plan.

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, and moves an earlier pending_migrations.jl aside to pending_migrations.jl.discarded (through discard_pending_migration), since that plan no longer describes any change (#727). The one exception is a plan a previous migrate already applied but failed to archive — its checksum matches the latest applied migration — which is kept, with a warning, for the next migrate to archive without re-applying. A missing models file raises MissingConfigurationError (the String form). A failure reading the live schema raises the read's own error and writes no plan (#1018); an empty database is not a failure — it reads as no tables, and every model is planned.

On PostgreSQL, a plan needs PostgreSQL 11 (#1162; reading the schema alone, as inspectdb does, needs 9.6), and a plan holding a statement the server cannot run raises BackendCapabilityError and writes nothing (#1146): adding a generated column (generated_from) needs PostgreSQL 12, and removing generated_from (DROP EXPRESSION) needs 13.

A pending plan holding hand-written data steps — entries labelled Data (pre): … or Data (post): … — is neither overwritten nor moved aside: those steps exist only in that file, so makemigrations raises InvalidMigrationError naming them instead (#740). Apply the plan with migrate first, or move the steps out of it, then plan again.

See also migrate, get_migration_plan, and the Database Migrations in PormG guide.

source
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.

source
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.

Raises InvalidMigrationError when no record has version, and changes nothing.

source
PormG.Migrations.migrate — Method
migrate(connection::PormGBackend, settings; interactive, destructive, name, lock_wait, lock_timeout, statement_timeout, atomic) -> MigrationResult
migrate(db::String; config, kwargs...) -> MigrationResult

Apply the pending migration plan (pending_migrations.jl) to a database (PostgreSQL, SQLite or MySQL), and return a MigrationResult saying what happened. Safe to call at application boot by several instances at once — see the Deploying guide.

Lifecycle

  1. Validate: change_db, then read and order the plan from disk and detect destructive statements. On PostgreSQL, a statement the server cannot run raises BackendCapabilityError here (#1146): a generated column needs PostgreSQL 12, and DROP EXPRESSION (removing generated_from) needs 13. Nothing is written to the database before step 3.
  2. Confirm: the schema precondition (#739 — the tables the plan header records are read again and compared, and a difference raises PlanPreconditionError; skipped for the #81 case below), then the lossy-ALTER row pre-check (#803 — a read-only count, refused whatever destructive says), then the destructive guard + interactive confirmation (TTY-aware — see _confirm_migration). Before the lock, so a prompt waiting on a human never holds it.
  3. Lock (PostgreSQL): the advisory lock MIGRATION_LOCK_KEY, waited on for up to lock_wait. Everything below runs while it is held (#737).
  4. Bootstrap: create pormg_migrations if needed and install configured extensions — also when nothing is pending.
  5. Execute: skip a plan whose checksum is the latest applied migration (#81); otherwise check the schema precondition again — this is the check that decides, since another instance may have changed the schema since step 2 — then run the plan and record it in pormg_migrations, in one transaction (PostgreSQL and SQLite have transactional DDL; see the Backend Capabilities page). A refusal writes no failed row: no statement was attempted.
  6. Archive: move the plan to applied_migrations/.

SQLite has no advisory lock, so steps 4–6 run unlocked there; its #81 check and the deciding precondition run inside the BEGIN IMMEDIATE write transaction instead, so two processes that race on one file cannot both apply the same plan, nor change a table between the check and the apply.

MySQL (#1268) takes a named lock (GET_LOCK) in step 3, but has no transactional DDL: each statement commits on its own. A plan of more than one statement is refused in step 1 with BackendCapabilityError unless atomic = false (or it is the latest applied plan, the #81 case); then step 5 runs the statements in order and records the plan once all of them ran, or as failed when one raised, leaving the ones before it applied. makemigrations afterwards plans what is still missing. Step 2's schema precondition is skipped there, because another instance's half-applied plan reads as a changed schema; the check inside the lock, after the #81 guard, decides.

A plan with no # pormg-schema-table: header — one generated before #739, or one whose lines were deleted on purpose — is applied without the precondition.

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 and migrate() never blocks on readline().
  • destructive::Bool=false: must be true to apply a plan is_destructive flags — any DROP (table, column, constraint, index, view, …), a TRUNCATE, or a DELETE with no WHERE — or one whose header records a column change that silently alters existing values (a lower decimal_places, float to integer, timestamp to date; see LOSSY_ALTER_KINDS). A destructive plan in a non-interactive context throws DestructiveMigrationError unless this is set. It does not get past a column change that would fail on existing rows: that is MigrationPrecheckError.
  • name::String="pending_migration": name for this migration in the history table.
  • lock_wait::Real=30: seconds to wait for another instance's migration to finish (PostgreSQL). On timeout migrate throws OperationalError, naming the process that holds the lock.
  • lock_timeout::Union{Real,Nothing}=nothing: seconds a plan statement may wait for a table lock before the migration fails and rolls back (PostgreSQL SET LOCAL lock_timeout). Without it, an ALTER queued behind a long-running query waits — and blocks every query on that table — for as long as that query runs.
  • statement_timeout::Union{Real,Nothing}=nothing: seconds any one plan statement may run before the migration fails and rolls back (PostgreSQL SET LOCAL statement_timeout).
  • atomic::Bool=true: false accepts a plan that cannot apply whole — one of more than one statement on a backend without transactional DDL (MySQL, transactional_ddl). PostgreSQL and SQLite always apply a plan whole, so there it changes nothing.

lock_timeout and statement_timeout are PostgreSQL's. SQLite accepts and ignores them, so one deploy script can run on both. On MySQL lock_timeout sets lock_wait_timeout (whole seconds) on the connection the plan runs on, and statement_timeout raises BackendCapabilityError (statement_timeouts): MySQL bounds no DDL statement's run time. lock_wait bounds the wait for the lock on PostgreSQL and MySQL. Each must be a positive number of seconds.

Use dry_run to inspect a plan without applying it.

source
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.

Raises InvalidMigrationError when no record has version, and changes nothing.

source
PormG.Migrations.run_once — Method
run_once(f, db::String, name; transaction = true, lock_wait = 30, config) -> Symbol
run_once(f, connection, settings, name; transaction = true, lock_wait = 30) -> Symbol

Run the data step f once per database, recorded by name (#740). f receives the database's pool, so raw SQL inside the step is fetch(conn, sql); ORM calls inside it join the step's transaction on their own.

PormG.Migrations.run_once("db", "2026-10-02_backfill_driver_code") do conn
    for d in M.Driver.objects.filter("code__@isnull" => true).list()
        M.Driver.objects.filter("driverid" => d[:driverid]).
            update("code" => uppercase(first(d[:surname], 3)))
    end
end

Returns :applied when this call ran the step, :already_applied when a step of that name is recorded already (f is not called), and :disabled on a change_db: false connection, where nothing is read or written — the same contract as migrate, which a boot script calls beside it.

  • Recorded in pormg_migrations_data, one row per name, created on first use. The row is the whole of "applied": a step is never re-run, whatever its code says now, so give a changed step a new name. There is no down step.
  • The migration lock. On PostgreSQL and MySQL it holds the lock migrate holds (pormg::migrations: an advisory lock, GET_LOCK on MySQL), waiting up to lock_wait seconds (OperationalError naming the holder after that), so a data step and a schema migration never run at once and two instances cannot both run one step. SQLite has no such lock; there the transactional step checks its record inside its BEGIN IMMEDIATE transaction, which no other process can share.
  • transaction = true (default): the step and its record commit together. If f throws, both roll back, nothing is recorded, the error propagates, and the next call runs the step again. On MySQL a DDL statement inside f commits the transaction implicitly, so the writes before it stay and the step is no longer one unit: keep a MySQL data step to writes.
  • transaction = false: for what cannot run inside a transaction — PostgreSQL's CREATE INDEX CONCURRENTLY — or a backfill too large for one. The record is written after f returns, so a throw records nothing and keeps whatever f already did: write such a step to be safe to run again (CREATE INDEX CONCURRENTLY IF NOT EXISTS). On SQLite nothing serializes it: two runners can both run f, and the one that records second returns :already_applied. An index a step creates must also be declared on the model (db_index = true), or the next makemigrations plans to drop it.

Raises TransactionError when called inside an open transaction on the same database — a step's commit has to be its own — and InvalidValueError for an empty name or one longer than 255 characters. It reads no files, so a register_connection database works too.

Expand → backfill → contract is two plans with a run_once between them: see Data Migrations in the advanced migrations guide.

source
PormG.Migrations.status — Method
status(connection, settings) -> MigrationStatus

Report migration status: applied, failed, pending, and drift signals.

The drift signals are about the migration history, not the schema: a missing history table, failed records, a pending plan beside applied history, and recorded migrations over a database with no user tables at all. When that last check cannot read the live tables, status() still returns, and says so in a signal naming the error type (#1076) — a check that could not look is not a clean one.

A failed record counts only while nothing has resolved it (#739). Failures do not block the next migrate, and a plan runs in one transaction on both engines, so a failed row means nothing was applied; once a later run applies the same plan (the same checksum), the failure moves to superseded and its drift signal clears. A failure no later apply matches stays in failed.

data_steps lists the run_once steps recorded in pormg_migrations_data (#740), oldest first — empty until the first one runs.

status() does not compare the live schema with the models — that is check(db; kinds = [:schema_drift]). It is read-only, and does not create the history table.

source
PormG.Configuration.close_all! — Method
close_all!() -> Nothing

Close every registered connection pool (#1229), without waiting for connections still checked out. The entries stay registered, so a later query reopens a pool. The public name of what PormG runs at process exit; a script or a test calls it to release its connections early.

source
PormG.Configuration.close_pool! — Method
close_pool!(pool::_SQLEngine; drain_seconds = 5.0)
close_pool!(db::String; drain_seconds = 5.0)

Close all connections in the database pool. Never throws; the pool stays usable afterwards.

Connections that are still checked out get a bounded grace period (#47): the call waits up to drain_seconds for their borrowers to return them, then closes whatever is still leased and logs one @warn with the count — those borrowers' in-flight queries fail. Pass drain_seconds = 0 to skip the wait (the atexit hook does, since nobody releases at exit). The string form tolerates a never-built pool. The full contract, including how a SQLite handle the async worker is still on is deferred rather than freed under it, is on ConnectionPool.close_pool!.

source
PormG.Configuration.connection — Method
connection(key::String) -> pool
connection(; key::String = "db") -> pool

The connection pool registered under key. The positional form matches get_settings(key), ping(key) and status(key) (#1232); the keyword form is kept for existing callers and is legacy.

source
PormG.Configuration.current_transaction_depth — Method
current_transaction_depth() -> Int

Return 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).

source
PormG.Configuration.ensure_before_connect! — Method
ensure_before_connect!(pool) -> Bool

Run the before_connect hook before opening a physical connection: the key's own hook from load(key; before_connect = f) (#1232), else the one registered with set_before_connect_hook. When no hook is registered, connection setup proceeds normally.

source
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.

This answers whether any transaction is open, on whichever database, and returns the innermost block's connection. It does not say which pool that connection belongs to. Code that is about to execute on a specific pool must use transaction_connection_for(pool) instead, which returns the connection of the open transaction on that pool, looking through enclosing blocks on other databases. Handing this one to another database runs the statement on the wrong database, possibly through the other engine's driver (#831).

source
PormG.Configuration.is_loaded — Method
is_loaded(path_or_key::String) -> Bool

Return true when a static configuration folder or dynamic connection key has already been registered in the in-memory configuration cache.

source
PormG.Configuration.load_many — Method
load_many(paths::AbstractVector{<:AbstractString}; env = nothing, root = nothing, force = false,
          before_connect = nothing)

Load several static configuration folders using the same environment override, and the same root when one is given (see load). Returns the list of connection keys that were loaded. A folder already loaded with the same environment is kept as it is, pool included, unless force = true (see load, #1225).

The server form loads short keys from a known project root, whatever the working directory:

PormG.Configuration.load_many(["db", "db_analytics"]; root = APP_ROOT, env = "prod")
source
PormG.Configuration.loaded_keys — Method
loaded_keys() -> Vector{String}

The connection keys PormG has registered — static (load, load_many, register_offline) and dynamic (register_connection) — sorted, so the answer is the same on every run.

PormG.Configuration.load("db"; env = "dev")
PormG.Configuration.loaded_keys()   # ["db"]

The public way to ask what PormG.config holds (#1229); the dictionary itself is internal.

source
PormG.Configuration.ping — Method
ping(path_or_key::String) -> Bool

Check 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.

source
PormG.Configuration.redact_secret — Method
redact_secret(conn_str::AbstractString) -> String

Mask the credentials in a connection string before it is logged, displayed, or embedded in an exception. Both dialects PormG accepts are handled:

  • keyword/value DSN (libpq conninfo) — password=… and user=… become password=**** and user=****, case-insensitively. Spaces around the = are allowed (libpq permits them), a quoted value (password='s3 cret') is masked whole, a backslash escape does not end the value, and a value containing unescaped whitespace is masked up to the next key= token rather than to the first space.
  • URL DSN — scheme://user:password@host… becomes scheme://****:****@host…, and scheme://user@host… becomes scheme://****@host…. Only the userinfo section is touched: scheme, host, port, database and query parameters survive, so a connect failure still shows where it was pointing.

Where it errs, it errs toward masking more. Three cases do so deliberately, because the alternative in each is a leak: credentials passed as URL query parameters (?user=u&password=p) swallow the parameters that follow them; an empty value (password= dbname=f1) swallows the next token; and a line carrying both a :// and a later @ masks between them. Under-redaction is the bug class this rule exists to prevent, so over-redaction is the direction it fails in.

A byte that is not valid UTF-8 is read as one more credential byte, which is how libpq reads it, so it never ends a masked value early.

Redaction is idempotent: it repeats its own rule until the string stops changing, so applying it again changes nothing. The repetition is bounded; on an input that exhausts the bound, a second call can only mask more, never reveal more.

This is the only redaction rule in PormG. Every site that puts a connection string into a log, an exception, a show, or a JSON document routes through it, so a new dialect is taught here once rather than at the call site.

julia> PormG.Configuration.redact_secret("host=localhost password=s3cret user=pingo")
"host=localhost password=**** user=****"

julia> PormG.Configuration.redact_secret("postgresql://pingo:s3cret@localhost:5432/f1")
"postgresql://****:****@localhost:5432/f1"
source
PormG.Configuration.register_connection — Method
register_connection(key::String, url::String; adapter::String = "PostgreSQL", pool_size::Int = 3, env = nothing)

Register a new database connection pool dynamically using a connection URL. Useful for multi-tenant applications or connecting to dynamic data sources.

env is the environment the entry records — what env(key) and status(key) report. The rule is load's (#1228, #1256): env = when passed, else a non-blank PORMG_ENV, else — in an interactive session (a REPL) only — "dev", with an @info saying so. A service, a test or a script that states neither gets InvalidConfigurationError, before any pool is built or replaced. A connection resolver (set_connection_resolver) states it with an "env" entry in the Dict it returns.

postgres_driver picks the driver of a PostgreSQL pool, as the postgres_driver: key does in connection.yml: "LibPQ" or "Postgres". Unset, it falls back to the PORMG_POSTGRES_DRIVER environment variable, then LibPQ.

change_data and change_db are the config: settings of the same names in connection.yml: change_data = true lets the entry write (create, update, delete, bulk writes), and change_db = true lets it run makemigrations/migrate. Both default to false, so an entry is read-only unless it says otherwise. A connection resolver states them with "change_data" and "change_db" entries in the Dict it returns.

PormG.Configuration.register_connection("f1", "postgresql://f1:…@db.internal/f1";
                                        env = "prod", change_data = true)
source
PormG.Configuration.register_offline — Method
register_offline(key; adapter = :postgres, server_version = nothing, django_prefix = nothing,
                 change_data = true) -> String

Register key as a connection that renders SQL but never connects (#1229) — for golden-SQL tests and inspect_query/show_query without a database. adapter (:postgres or :sqlite) picks the dialect. Models bind to it like any loaded configuration: @import_models/set_models with a folder named key, or Models.bind!(model, key). Executing a query on it raises InvalidConfigurationError. Remove it with unregister_connection(key).

PormG.Configuration.register_offline("db")
PormG.@import_models "db/models.jl" models
sql = PormG.inspect_query(models.Driver.objects.filter("surname" => "Senna"))
PormG.Configuration.unregister_connection("db")

Registering the same key again replaces the offline entry. A key that load or register_connection registered is refused, not overwritten, and so is the reverse: load of an offline key raises InvalidConfigurationError (#1451).

An offline SQLite key states its version (#1452). A few clauses depend on the SQLite version (NULLS FIRST/LAST from 3.30.0, window functions from 3.25.0, RIGHT/FULL joins from 3.39.0, and the bind-parameter limit a bulk statement is chunked by). An offline key renders them for server_version, in SQLite's packed form (3_045_000 for 3.45.0), never for the library of the machine that runs it, so a pinned SQL text is the same on every machine and needs no driver. Without server_version, a query that needs the version raises InvalidConfigurationError naming it (a bulk write always does, whatever its row count); every other query renders. PostgreSQL's query SQL depends on no server version, and server_version with adapter = :postgres raises InvalidConfigurationError.

PormG.Configuration.register_offline("db"; adapter = :sqlite, server_version = 3_045_000)

The entry's environment is always "dev" — env(key) and status(key) report it — whatever PORMG_ENV says (#1256). An offline entry reads no connection.yml, so there is no environment block for anything to select; the value is a constant, not the load fallback, and no session-dependent rule applies.

source
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_legacy". To single out connections created by register_connection, check settings.dynamic rather than the folder name.

settings.db_def_folder is a path, and the configuration key is the key argument. The two coincide only for a load("db") without root: load("db"; root = APP_ROOT) registers "db" with an absolute folder (#857), so compare key == "db" or the folder's basename, never settings.db_def_folder == "db".

Typical uses include VPN setup, credential refresh, or SSH tunnel activation. When no hook is registered, connections proceed normally. A key loaded with its own hook — load(key; before_connect = f) (#1232) — uses that one instead, so a hook for one connection no longer has to test key == "…" and be registered from both the module body and __init__.

source
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:

  • nothing if the key cannot be resolved.
  • A Tuple of (url, adapter, pool_size) or a Dict with those keys.
source
PormG.Configuration.status — Method
status(path_or_key::String) -> NamedTuple

Return a compact server-oriented status payload describing whether a configuration is loaded and reachable. implicit is true for an entry that Models.set_models minted through its implicit load rather than one the application loaded itself (#553). dynamic is true for an entry created by register_connection rather than loaded from a folder (#623).

source
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.

This does not start a transaction

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")
end

The context nests: depth increments by one per block (so current_transaction_depth drives savepoint naming), and a nested block on the same pool inherits the outer block's SQLite reserved-primary-key reservations rather than starting a fresh table. A block on a different pool starts a fresh table, because the reservations are keyed by table name only (#831). Being a ScopedValue it is dynamically scoped — tasks spawned inside the block inherit it, and it unwinds automatically, including on a throw. A spawned task that is still running after the block ends no longer sees it as open: its statements run in autocommit on a connection of their own (#839).

source
PormG.ConnectionPool.FetchTask — Type
FetchTask

A 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 (the LibPQ extension's wrapper around a LibPQ.AsyncResult, the Postgres.jl extension's handle, or a Task for SQLite)
  • pool::_SQLEngine: The connection pool to release the connection to
  • conn: The driver connection being used for this query
  • completed::Bool: Whether the async result has been awaited
  • result_cache::Union{Nothing, Any}: Cached result for multiple await_result calls
  • in_transaction::Bool: Whether this task is part of a transaction (don't release connection)
  • borrowed::Bool: Whether the caller passed the connection in as conn (#970). A borrowed connection is the caller's, so await_result never releases it — on success, on a failure, or after a cancelled await.
  • abandoned::Bool: Whether the await was cut short by a cancellation rather than a database failure (#315). Set by await_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 @autodocs publishes this docstring to api.md, and a cross-reference from there to an internal helper cannot resolve — Private = false means the target is never rendered. That broke the docs build once.)
source
PormG.ConnectionPool.PoolConnectError — Type
PoolConnectError <: Exception

Thrown 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.

A connection string the driver cannot even parse — a NUL byte, an unquoted space inside a value, a malformed percent-escape in a URL — is permanent too, and its cause is an InvalidConfigurationError whose message masks the fragment libpq quoted (#657). Every rendering of the error — showerror, show/repr/string, and PormG's own log lines — passes the cause through redact_secret; the cause field itself is the raw object, so reflecting on it (e.cause, dump, getfield) is outside that guarantee.

source
PormG.ConnectionPool.PoolTimeoutError — Type
PoolTimeoutError <: Exception

Thrown 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.

source
Base.fetch — Method
fetch(connection::_SQLEngine, sql::String; params=nothing, ignore_tx=false) -> Tables.rowtable

Execute a database query synchronously (blocking). Internally uses async execution but immediately awaits the result.

conn = c runs the statement on your own leased connection, which is borrowed: it is never released here, and a dropped connection is not retried — see acquire_connection.

Without conn and outside a transaction, a dropped connection renews the failed connection and retires the pool's idle ones. The statement is retried once only when it provably never ran: the server reported the backend gone with an error code, or the driver refused before sending it. Any other drop raises OperationalError, because the statement may already have run (#1042). Wrap a read you know is safe to run twice in with_read_retry to retry it.

source
PormG.ConnectionPool.acquire_connection — Method
acquire_connection(pool::PormGPostgres; timeout_seconds=nothing, max_retries=300)
acquire_connection(pool::PormGMySQL; 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)
end

Most 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.

Passing your connection to a funnel: it is borrowed

fetch, fetch_async, with_transaction and with_transaction_async accept the connection as conn = c. A funnel never releases a connection you pass — not after the statement, not when a check refuses it before it is sent, not when the driver throws, and not after a cancelled await. It stays yours, and you release it exactly once (#970). The single exception is an explicit hand-over, with_transaction(…; conn = c, release_conn = true). A connection the funnel acquired itself (conn = nothing) is the funnel's: fetch releases it, with_transaction* returns it to you in the result tuple.

If an await on your connection was cut short by a cancellation (Ctrl+C), the driver may still be using it: do not release it as it is — pass it to finalize_transaction_connection! with renew = true.

Keyword arguments

  • timeout_seconds: how long to wait for a free connection. Defaults to the pool's pool_timeout from connection.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. The paced connect retries below do not count against it; the timeout bounds them.
  • 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 with mode = :write; :read or :any can hand you a read-only handle and the write will fail. Any other symbol raises InvalidValueError.

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. Any other failed connect (a server restarting, a refused or timed-out connect) is retried with a backoff of 50 ms doubling to 0.5 s until the timeout, which then raises PoolConnectError carrying the last cause; a release meanwhile still hands the caller its slot at once. A connect runs without holding the pool's lock (#1540): the caller reserves its slot, so other callers release, borrow and time out while it connects.

See also release_connection, pool_stats, and the Advanced Configuration guide for pool tuning and leak detection.

source
PormG.ConnectionPool.atomic — Method
atomic(f::Function, db; durable::Bool=false) -> result

Run 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
    # swallows the validation error; the outer transaction is still usable here
  end
end

Pass durable=true to require this block be the outermost transaction on its database — it throws if a transaction is already active on the same database (mirrors Django's atomic(durable=True), which is checked per connection). A transaction open on a different database does not count.

source
PormG.ConnectionPool.await_result — Method
await_result(ft::FetchTask) -> result

Await the completion of an async fetch task and return the result. Releases the connection back to the pool when the task acquired it. A connection the caller passed in as conn is borrowed and stays leased, on success and on failure — see acquire_connection for the ownership rule (#970).

This function is idempotent - calling it multiple times returns the same result without re-releasing the connection.

source
PormG.ConnectionPool.fetch_async — Method
fetch_async(connection::PormGPostgres, sql::String; params=nothing) -> FetchTask

Start an async database query that yields to the Julia scheduler. Returns a FetchTask that can be awaited with await_result().

conn = c runs the statement on your own leased connection, which is borrowed: it is never released here — see acquire_connection. It also bypasses an open transaction context.

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)
source
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, and return the number of rows copied (the COPY n command tag, #670). The driver-specific streaming (LibPQ.CopyIn + result drain) lives in the PostgreSQL extension as backend_copy_in!.

source
PormG.ConnectionPool.finalize_transaction_connection! — Method
finalize_transaction_connection!(pool, conn; rollback_error=nothing, renew=false,
                                 body_error=nothing, abandoned_await=false) -> Nothing

Terminal 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.

Pass the error the transaction's body raised as body_error (the one your catch saw before the cleanup ROLLBACK). On Postgres.jl, a Ctrl+C that lands in the driver's read closes the connection, so the ROLLBACK fails at once and the connection is renewed while the server keeps running the interrupted statement, with its locks held. When body_error is an interrupted await and rollback_error is a lost connection, the statement is cancelled first (#1588, #1638). Pass abandoned_await=true when you know a statement's await in the body was interrupted although the error that reached you is another one (run_in_transaction does, for a body that caught the interrupt). Both are optional: without them the connection is returned exactly as before.

source
PormG.ConnectionPool.pool_stats — Method
pool_stats(pool) -> NamedTuple

A 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.

source
PormG.ConnectionPool.release_connection — Method
release_connection(pool, conn) -> Bool

Return 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).

Not for a transaction that may have failed

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.

source
PormG.ConnectionPool.run_in_transaction — Method
run_in_transaction(f::Function, pool::PormGPostgres) -> result

Execute 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:

  1. All queries use the same connection (transaction isolation)
  2. Automatic COMMIT on success
  3. Automatic ROLLBACK on error
  4. 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)
end

Every column above is one the F1 models declare null = false; omitting any of them raises at create() before the statement is built.

source
PormG.ConnectionPool.with_read_retry — Method
with_read_retry(f::Function; attempts::Integer = 1, backoff::Real = 0,
                inside_transaction::Symbol = :raise) -> result

Run f() and, when it raises OperationalError, run it again — at most attempts more times, sleeping backoff seconds before each retry. With the default attempts = 1, f runs at most twice. Each retry is logged as a @warn carrying the error's type (error), its SQLSTATE and its message (msg, error_message, which never quotes a value), the retry number and attempts. The exception itself is not logged: its driver cause can quote the statement's values.

rows = with_read_retry() do
  M.Result.objects.filter("raceid" => 1120).list()
end

You vouch that f is safe to run twice. PormG does not retry a statement after a connection drops mid-flight, because the statement may already have run (#1042), and it does not try to tell a safe statement from an unsafe one: the SQL text cannot prove it. A SELECT that calls nextval() or a function with side effects, or a WITH query that inserts rows, is not safe to repeat. Wrap only the reads you know are.

  • Only OperationalError is retried: a dropped connection, after which the pool has already recovered, and the other transient refusals it covers — a deadlock, a lock or advisory-lock timeout, SQLite's database is locked. A cancelled statement is not retried, on any engine: running it again would defeat the cancel. On PostgreSQL that is SQLSTATE 57014 (a statement_timeout, a pg_cancel_backend); on MySQL, error 1317 (a KILL QUERY, which is also how PormG cancels an abandoned statement there) and error 3024 (max_execution_time). IntegrityError, InvalidValueError, a query-build error or any other exception propagates on the first raise.
  • f must raise the OperationalError itself. One raised inside a task f spawns arrives wrapped (TaskFailedException, CompositeException) and is not retried; PormG's own terminals unwrap.
  • Inside an open transaction it raises TransactionError without running f: a lost connection loses the whole transaction, so retry the transaction — the atomic block — not one statement in it (#138).
  • inside_transaction = :run_once runs f once there instead, with no retry and no error; an OperationalError it raises propagates, so the enclosing atomic block rolls back. It is for a library that reads on an application's behalf and cannot know whether the application's own transaction is open around the call. Any open transaction counts, on any database, as for the default. Outside a transaction the block is retried as usual, so it must still be safe to run twice. Any Symbol other than :raise (the default) or :run_once raises InvalidValueError.
  • When every attempt fails, the last OperationalError propagates.

See also atomic.

source
PormG.ConnectionPool.with_savepoint — Method
with_savepoint(f::Function, settings::PormGSettings, name::String) -> result

Execute 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).

source
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.

Prefer `run_in_transaction`

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. When nothing, one is acquired — on SQLite with mode = :write, since a transaction writes. A connection you pass is borrowed: this call never releases it unless release_conn = true (see acquire_connection, #970).
  • release_conn: return the connection to the pool when this call finishes — the one explicit hand-over of a connection you passed. Leave it false while the transaction is still open; you receive conn back in the return tuple.
  • params: bound query parameters. Never interpolate values into sql. A plain vector or tuple of values works as it does for fetch (#218): write the backend's own placeholders ($1, $2 on PostgreSQL, ? on SQLite), since PormG translates none, and nothing binds as NULL.

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).

`release_conn=true` on a COMMIT/ROLLBACK is a use-after-release race

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.

source
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. A conn you pass is borrowed and stays yours on every outcome, a driver error included (#970); a connection this call acquired is released only when it throws before returning it — see acquire_connection.

For transactions, you typically want to keep the connection for multiple queries.

source
PormG.ConnectionPool.without_foreign_keys — Method
without_foreign_keys(f::Function, db; check_on_exit::Bool = true) -> result

Run 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 PostgreSQL and SQLite, so a block that is transiently inconsistent — writing children before their parents — commits without any special handling. MySQL checks a foreign key at each statement and has no deferral, so there this raises BackendCapabilityError (deferred_foreign_keys): write the parents first. Reach for this only when that is not enough: a repair that must leave a violation in place, or a test that plants one deliberately.

It is still one transaction on both backends: nothing inside f() commits until the whole block does, so it is no way to split a load too large for a single transaction.

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, on both backends. It cannot nest inside run_in_transaction/atomic and raises TransactionError before touching the database if you try. On SQLite a nested call could not suspend anything — PRAGMA foreign_keys = OFF is silently ignored while a transaction is open. On PostgreSQL it would add nothing for the foreign keys PormG creates — they are already deferred to COMMIT inside a transaction — and its SET CONSTRAINTS ALL DEFERRED would outlive the block: it persists after the savepoint is released, into the rest of the enclosing transaction.

With check_on_exit = true (the default) SQLite runs a PRAGMA foreign_key_check 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

# Plant an orphan in a SQLite test database, to prove a consistency report finds it.
# check_on_exit = false because the orphan is the point of the block.
without_foreign_keys("db"; check_on_exit = false) do
    M.Result.objects.filter("resultid" => 1).update("raceid" => 999_999)
end

# Write it as the outermost block — nested, it raises TransactionError on both backends:
atomic("db") do
    without_foreign_keys(() -> nothing, "db")   # TransactionError
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
end
Backend divergence

SQLite 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 an orphan left at the end is refused differently: SQLite rolls back with UnsafeMutationError when check_on_exit = true (and commits it when false), while PostgreSQL's COMMIT fails with IntegrityError whatever check_on_exit says — a violation cannot be committed there at all.

See also atomic, run_in_transaction.

source
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
  • Declares the model set (Models.@models …, #1602) from the file's top-level X = Models.Model(…) assignments (documented, typed, const or global), when the file does not end with its own Models.@models line. A file that builds a model any other way (in a loop, a function, an @eval) and writes no line is refused with ModelDefinitionError, since the generated line would leave that model out; a model reached through using is registered only when the file declares it
  • Registers models for the current session, recording the key in the module's model set, from which a restored package image rebinds on the first query (Models.ensure_model_initialized)
  • Handles World Age issues in Julia 1.12+: the registration is its own top-level statement

Users do NOT need to add an __init__() function to their model files, and none is added for them (#1607).

With Revise loaded, it also registers a reload of the file (Models.set_models(…; source), #1487). It is the REPL / script form: in a package's module body it runs at precompile time, where no reload is registered, so a package binds its models from __init__ with Models.set_models.

Example

PormG.Configuration.load("db"; env = "dev")   # the configuration first
PormG.@import_models "db/models.jl" models
import .models as M

# Now use M.Driver, M.Result, etc.
source
PormG.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. The model set (Models.@models, #1602) is declared for you from the block's top-level X = Models.Model(…) assignments, unless the block declares its own. A block that builds a model any other way (in a loop, a function) and declares none is refused with ModelDefinitionError: write its Models.@models line.

Example

@models_module MyDB "db" begin
    Driver::Models.ModelType = Models.Model("drivers",
        id = Models.IDField(),
        name = Models.CharField()
    )
end
source