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(10)
.offset(n)Skip the first n rows..offset(20)
.page(limit) / .page(limit, offset)Limit, or limit and offset in one call. The one-argument form leaves any .offset() already set untouched..page(20, 40) / .page(20)
.distinct()Add DISTINCT to the SELECT..distinct()
.db("key")Route the query to a different connection pool..db("tenant_42")
.on("path", key => value)Add predicates to the ON clause of an existing join path..on("driverid", "nationality" => "British")
.cjoin("field" => "Model", ...)Add a custom join at query time..cjoin("driverid" => "Driver")
.cjoin_on("Model"; alias, on, join_type)Anchor-less join: on is the entire ON clause..cjoin_on("Driver"; alias = "d", on = [...])
.with("name" => subquery; join_field, join_type)Define one CTE on the query; call again for a second. Its columns are then reached with CTE(name, path)..with("fast" => sub)
.select_for_update(; nowait, skip_locked, no_key)SELECT … FOR UPDATE row lock (PostgreSQL; must run inside a transaction)..select_for_update(nowait = true)
.copy()Deep-copy the handler to branch a chain without disturbing the original.base.copy().filter("year" => 2020)

See also: Custom Joins for .cjoin() / .cjoin_on() / .on(), and Subqueries and CTEs for .with() and the CTE reference object.

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.
query |> DataFrameDataFramePipe to DataFrame for tabular output.
.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.
.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).

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.
:dictReturns full metadata dictionary (sql, parameters, dialect, model, operation, etc.).
:inspectionAlias of :dict. Useful when you want the same rich metadata shape used by inspect_query().
:paramsReturns the parameters array only.
:noneReturns nothing. Zero-overhead mode for benchmarking the builder itself.
query = M.Driver.objects.filter("nationality" => "British")

# Get just the SQL string
sql = query.list(show_query=:sql)

# Benchmark the builder without execution overhead
@time query.list(show_query=:none)

# Get full metadata
meta = query.list(show_query=:dict)

inspect_query

Dedicated API for comprehensive query inspection without executing. Features a heuristic intent detector that guesses the operation type (select, insert, update) based on the object state.

query = M.Driver.objects.filter("nationality" => "Brazilian").order_by("surname")
inspection = query |> inspect_query()

println(inspection[:sql_text])   # The generated SQL
println(inspection[:parameters]) # Bound parameters
println(inspection[:operation])  # Automatically detects :select
println(inspection[:dialect])    # :postgresql or :sqlite
Note

LIMIT and OFFSET values are rendered as literal integers in the SQL string. They do not appear in inspection[:parameter_buckets] or inspection[:parameters]. This is by design.


Filter Operators

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

Comparison Operators

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"

Transform Functions

TransformDescriptionExample
field__@yearExtract year from date"dob__@year" => 1960
field__@monthExtract month from date"dob__@month" => 3
field__@dayExtract day from date"dob__@day" => 21
field__@quarterExtract quarter (1-4)"date__@quarter" => 1
field__@dateExtract date from datetime"created_at__@date" => Date(2025, 1, 1)

For the full list of operators and transforms, see Filters and Aggregates.


F-Expressions

F() enables database-side field references and arithmetic. Use it for field-to-field comparisons and computed expressions.

using PormG: F
using PormG.Functions: Sum, Count

# Field-to-field comparison
M.Result.objects.filter(F("grid") == F("positionorder"))

# Arithmetic in projections
M.Result.objects.values(
    "driverid__surname",
    "bonus" => F("points") * 0.1
)

# Aggregate ratios
M.Result.objects.values(
    "driverid__surname",
    "avg_pts" => Sum("points") / Count("resultid")
)

# Atomic update (no read-modify-write race)
M.Result.objects.filter("resultid" => 1).update("points" => F("points") + 10)

# Date arithmetic with explicit Julia durations (or the Interval helper)
using Dates
M.Race.objects.filter("raceid" => 1).update("date" => F("date") + (Month(1) + Day(15)))

+/- accept Dates durations (Day, Month, Year, …) and Interval(...) for cross-database date math. See Field Expressions for the full reference.


Q Objects: Complex Boolean Logic

Q() and Qor() enable complex boolean predicates with AND/OR logic:

using PormG: Q, Qor

# AND logic (Q contains AND by default)
M.Driver.objects.filter(Q("nationality" => "Brazilian", "code" => "SEN"))

# OR logic
M.Driver.objects.filter(Qor("nationality" => "Brazilian", "nationality" => "French"))

# Nested AND/OR
M.Driver.objects.filter(
    Q("nationality" => "Brazilian", Qor("forename" => "Ayrton", "forename" => "Nelson"))
)

See Q Objects for the full reference.


Aggregate Functions

All aggregate functions can be used in .values() for grouping or combined with F-expressions:

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 values"full" => Concat("forename", Value(" "), "surname")
Trim("field")Trim whitespace"clean" => Trim("name")
LTrim("field")Left trim"clean" => LTrim("name")
RTrim("field")Right trim"clean" => RTrim("name")
Replace("field", old, new)Replace substring"fixed" => Replace("name", "-", " ")

Numeric Functions

FunctionDescription
Abs("field")Absolute value
Round("field", precision)Round to precision
Floor("field")Floor
Ceil("field")Ceiling
Sqrt("field")Square root
Exp("field")Exponential
Ln("field")Natural logarithm
Power("field", n)Raise to power
Mod("field", n)Modulo

Conditional & Utility Functions

FunctionDescriptionExample
Value(x)Literal value in SQLValue("hello")
Coalesce(args...)First non-null valueCoalesce("nickname", "forename")
NullIf("field", value)Returns NULL if equalNullIf("code", "")
Greatest(args...)Maximum of valuesGreatest("points", Value(0))
Least(args...)Minimum of valuesLeast("points", Value(100))
Cast("field", type)Type castingCast("points", "INTEGER")
Extract("field", "part")Extract date/time partExtract("dob", "year")
ToChar("field", fmt)Format to stringToChar("dob", "YYYY-MM")

Case Expressions

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

# CASE WHEN idade >= 60 THEN 'Sim' ELSE 'Não' END
"mais_60" => When("idade__@gte" => 60, then = "Sim", otherwise = "Não")

Multi-branch: wrap a vector of When fragments in Case:

"category" => Case([
    When("positionorder" => 1,            then = "Winner"),
    When("positionorder__@lte" => 3,      then = "Podium"),
], default = "Other")

Plain strings and numbers work as then, otherwise, and default values without Value().

See Functions and Dates for more details.


Custom Joins

.cjoin()

Defines custom join conditions at query time as a chainable method on the query handler. Useful for legacy databases, non-FK joins, and multi-tenant systems.

using PormG: Q, Qor

df = M.Result.objects.cjoin(
    "driverid" => "Driver",
    filters=[Q("nationality" => "Brazilian", Qor("forename" => "Ayrton", "forename" => "Nelson"))],
    join_type="INNER"
).values("driverid__forename", "driverid__surname", "points") |> 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".
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 existing join paths (including reverse joins) without redefining them:

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

Bulk Operations

bulk_insert

Inserts multiple records in a single operation. Returns the inserted rows.

df = DataFrame([
    Dict("forename" => "Ayrton", "surname" => "Senna", "nationality" => "Brazilian"),
    Dict("forename" => "Alain",  "surname" => "Prost", "nationality" => "French"),
])
result = bulk_insert(M.Driver, df)

Duplicates can be skipped or merged instead of erroring with the on_conflict= keyword (PostgreSQL and SQLite ≥ 3.24; see Conflict Handling):

bulk_insert(M.Status, df, on_conflict = :nothing)                                  # ON CONFLICT DO NOTHING
bulk_insert(M.Status, df, on_conflict = (action = :nothing, target = ["statusid"]))  # targeted skip
bulk_insert(M.Status, df,                                                          # upsert
    on_conflict = (action = :update, target = ["statusid"], set = ["status"]))

bulk_update

Updates multiple records in a single operation.

bulk_update(M.Result.objects, df_with_changes, columns=["points"], match_on=["resultid"])

Key contracts:

  • columns= names the participating fields and is the single place a DataFrame column is mapped to a model field ("df_col" => "field"); match_on= selects the per-row merge keys by bare model field name (a field in both is matched, never SET); filters= are constant predicates applied to every row.
  • A match_on field's source column is its columns= mapping when you declared one — the mapping is authoritative even if a same-named DataFrame column also exists (that case warns) — otherwise a DataFrame column with the field's own name. A value PormG auto-populates for an out-of-scope field (auto_now, or a 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.
  • DataFrame 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.
  • bulk_update() rebuilds the WHERE clause from match_on= and filters= and does not preserve filters that were already attached to the handler.
  • Constant lookup filters on base-table columns are supported, but relation traversals that would require JOINs are rejected.
  • Foreign-key columns accept scalar primary-key values, including 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.

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 in the block use the same connection
  • Automatic rollback on error

See Transactions for the full reference including savepoints and multithreaded patterns.

atomic

Friendly, Django-flavored alias for run_in_transaction. A nested atomic on the same database automatically becomes a SAVEPOINT, so a failing inner block rolls back only to its savepoint while the outer transaction survives (works identically on PostgreSQL and SQLite):

atomic("db") do
    M.Result.objects.create("raceid" => 1, "driverid" => 1, "points" => 25)
    try
        atomic("db") do                 # nested → SAVEPOINT
            M.Result.objects.create("raceid" => 1, "driverid" => 1, "points" => 18)
            error("validation failed")  # rolls back to the savepoint only
        end
    catch
        # outer transaction still usable here
    end
end

Pass durable = true to require the block be the outermost transaction (raises if one is already active).

select_for_update

Query-builder method that adds a FOR UPDATE clause to lock the selected rows until the surrounding transaction commits — the guard for a safe read-modify-write. Keyword options nowait, skip_locked, and no_key map to FOR UPDATE NOWAIT / FOR UPDATE SKIP LOCKED / FOR NO KEY UPDATE (nowait and skip_locked are mutually exclusive). On PostgreSQL it must run inside a transaction; on SQLite it is a silent no-op (no row-level locking). See Row-Level Locking.

atomic("db") do
    row = M.Constructor_standings.objects.
        filter("constructorstandingsid" => 1).
        select_for_update().
        list() |> first
    M.Constructor_standings.objects.
        filter("constructorstandingsid" => row[:constructorstandingsid]).
        update("points" => row[:points] + 25)
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); PormG does no translation, and a NULL is missing:

# PostgreSQL
task = fetch_async(settings, "SELECT count(*) FROM driver WHERE nationality = \$1", ["Brazilian"])
n    = await_result(task)
# SQLite: the same call with "... = ?" and the same ["Brazilian"] 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.

See the Async & Concurrency guide for fan-out patterns, @async vs Threads.@spawn, connection-pool sizing, and why you must not fan out queries inside a transaction.


Configuration API

Configuration.load(path; env=nothing)

Loads a database configuration folder. Use env to explicitly set the environment instead of relying on ENV["PORMG_ENV"].

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

Configuration.load_many(paths; env=nothing)

Bootstraps multiple database folders in one call:

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

Configuration.is_loaded(path_or_key)

Returns true if PormG has registered settings for the given folder/key. Does not open connections.

Configuration.ping(path_or_key)

Tests actual database reachability. Returns Bool.

Configuration.status(path_or_key)

Returns a rich status payload:

s = PormG.Configuration.status("db")
# (key="db", loaded=true, reachable=true, adapter="PostgreSQL", app_env="prod")

For the full configuration guide, see Configuration.


Advisory Locks

with_advisory_lock

Acquires a PostgreSQL advisory lock for distributed coordination:

PormG.with_advisory_lock("db", "migration_lock"; wait=true, timeout_ms=10000) do
    # Critical section — only one process at a time
    PormG.Migrations.migrate("db")
end

Strategies:

  • :poll (default) — Client-side polling with interval
  • :block — Server-side blocking via pg_advisory_lock

SQLite: no-op — the block executes without locking. It warns once per lock key; on_missing_lock=:ignore accepts that silently, on_missing_lock=:error raises BackendCapabilityError rather than running unprotected.

See Advisory Locks for the full reference.


Abstract Types

PormG's type hierarchy provides the foundation for the query builder and model system:

TypeDescription
PormGAbstractTypeBase abstract type for all PormG types.
PormGSettingsThe connection-settings/config type (Configuration.Settings is a subtype). Renamed from SQLConn.
PormGBackendBase for the database backend/dialect markers (subtypes: PormGPostgres, PormGSQLite), 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.
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, Interval, show_query, inspect_query

Rows & exceptions

PormGRow, pk, DoesNotExist, MultipleObjectsReturned

Error taxonomy

Every PormG misuse raises a subtype of PormGError (<: Exception) so callers can catch a type instead of matching on a message string. Catch PormGError for any PormG failure, or a specific subtype for a specific reaction (#231, completed in #239):

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

Database failures are wrapped too

The taxonomy has two halves. Most of it reports misuse of PormG, caught before anything is sent. DatabaseError reports what the database itself refused once a statement got there — a constraint violation, SQL the backend rejects, a connection dropped mid-query — so catch PormGError really does cover both, and an app never has to name SQLite.SQLiteException / LibPQ.Errors.* (which would mean depending on the driver package just to spell the type). The driver's own exception stays reachable on .cause (#268).

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 (which renders its statements too) — do not render from msg alone, so reaching for that field breaks on exactly the errors you are least likely to have tested against. See Error Handling for the per-type field list.

try
    M.Result.objects.filter("driverid__surname" => "Senna").update("points" => 25)
catch e
    e isa PormGError || rethrow()
    @error "PormG rejected the write" msg=error_message(e) type=typeof(e)
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
FieldAccessError (abstract)Umbrella for field/accessor lookup failures — catch it to get both cases below.
UnknownFieldErrorA field, alias, column, or __ lookup path does not exist on the model or projected row.
LazyTraversalErrorAn unprojected ForeignKey or OneToOneField was read off a fetched row — project it in values(...) first.
FilterErrorInvalid filter argument/shape, or an operator misused on a JSON/subquery column.
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 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_*), explicit window frame= on SQLite, bulk_copy on SQLite, with_advisory_lock(...; on_missing_lock = :error) on SQLite, or a too-old SQLite library. Change the query or the backend.
InvalidValueErrorA value failed coercion/type validation on insert/update, an identifier failed the safety check, or an interval/duration could not be parsed.
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 neither PostgreSQL nor SQLite reached an execution path — an internal PormG dispatch bug; please report it. (Capability limits are BackendCapabilityError; an unbound model is InvalidConfigurationError.)
DoesNotExist / 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, or an unresolvable ForeignKey target.

FieldValidationError fires while defining a model; InvalidValueError fires while coercing a value on the insert/update path. That is the distinction between the two.

Configuration and migrations

TypeRaised when
ConfigurationError (abstract)Umbrella for configuration failures — covers InvalidConfigurationError, MissingConfigurationError, and WritesDisabledError (listed in the Querying table above, where users meet it).
InvalidConfigurationErrorConfiguration is present but unusable — unsupported adapter, unknown connection key, malformed extensions, a model not bound to a connection (or bound to an entry whose pool was never built), a missing driver package, or an attempt to overwrite a static connection.
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 both cases below.
InvalidMigrationErrorA duplicate index name in a plan, an invalid answer to an interactive makemigrations prompt, or an unimplemented migrate_to(version) path.
DestructiveMigrationErrorA destructive plan was applied non-interactively without destructive=true; carries the refused statements. Not on the using PormG surface — name it PormG.Migrations.DestructiveMigrationError.
PoolError (abstract)Umbrella for connection-pool failures — catch it to get both cases below.
PoolTimeoutError / 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

Everything above reports misuse of PormG, raised before a statement leaves the process. These report what the database refused once it got there. Each carries adapter ("PostgreSQL" / "SQLite") and cause — the driver's own exception, kept so SQLSTATE-level detail stays reachable — instead of a msg, so read them with error_message.

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).
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) nested inside an open transaction, or touching a model bound to one connection while a transaction is open on another. Nothing was sent.
try
    M.Driver.objects.create("driverref" => "senna", "code" => "SEN")
catch e
    e isa IntegrityError   && return conflict(error_message(e))   # a constraint refused it
    e isa OperationalError && return retry_later()                # transient — try again
    rethrow()
end
Classification is exact on PostgreSQL, 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. Treat IntegrityError as reliable on both; .cause is there when you need more than PormG's three kinds.

Connect-time failure is not a DatabaseError — it never reached a statement, and arrives as PoolConnectError under PoolError. A failed migration ALTER TABLE is one: migrate() lets the StatementError through rather than re-wrapping it as MigrationError, which would bury the constraint detail — so catch DatabaseError alongside MigrationError around migrate().

The abstract umbrellas exist so the buckets have no holes: catch ConfigurationError also catches a missing connection.yml, and catch MigrationError also catches a refused destructive migration.

try
    M.Result.objects.update("points" => 25)   # no filter → refused, protects every row
catch e
    e isa PormG.UnsafeMutationError && @warn "add a filter before update()"
    e isa PormG.WritesDisabledError    && @warn "this connection is read-only"
    rethrow(e)
end

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

try
    PormG.Configuration.load("db_2")
    PormG.Migrations.migrate("db_2")
catch e
    e isa PormG.ConfigurationError && @error "check connection.yml" exception=e
    e isa PormG.MigrationError     && @error "migration refused"    exception=e
    rethrow(e)
end

Bulk Operations

bulk_insert, bulk_update, bulk_copy, allocate_primary_keys, resync_sequences

Async API

fetch_async, await_result, FetchTask — see Async & Concurrency

Transactions

run_in_transaction, atomic, with_savepoint, with_tx_context, in_transaction_context — plus PormG.ConnectionPool.with_transaction for hand-rolled lifecycles

Locking

with_advisory_lock

Connection pool

pool_stats, PoolTimeoutError, PoolConnectError — manual checkout via PormG.ConnectionPool.acquire_connection / release_connection (pair them in a finally; on SQLite a write needs mode = :write)

Utilities & lifecycle

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

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

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

`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, 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:

AggregateSum, Avg, Count, Max, Min — see Filters and Aggregates

ConditionalCase, When — see Functions and Dates

WindowWindowOver, WindowSpec, Rank, DenseRank, RowNumber, Lag, Lead, FirstValue, LastValue, NthValue — see Window Functions

StringConcat, Lower, Upper, Length, Replace, Trim, LTrim, RTrim

MathAbs, Round, Floor, Ceil, Sqrt, Exp, Ln, Power, Mod

Type / valueCast, Extract, ToChar, Value, Coalesce, Greatest, Least, NullIf

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
`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.install_ai_skillsFunction
install_ai_skills(target_dir::String = pwd())

Copy PormG's AI skill blueprint into the target project's .github/skills/pormg-usage/ directory. This helps AI assistants (GitHub Copilot, Claude, Cursor, and other agents) provide accurate PormG code suggestions.

The blueprint ships with the PormG package under .github/skills/pormg-usage/ and is a multi-file bundle — SKILL.md plus the supporting reference.md/writing.md it links to. The whole directory is copied so none of SKILL.md's relative links dangle after install.

source
PormG.setupFunction
setup(path::String = DB_PATH)

Interactively setup the connection.yml file for PormG. This will prompt for database adapter, name, and connection details.

source
PormG.upgrade_guideFunction
upgrade_guide([io::IO = stdout]; from, to = <current code>, structured = false)

Print the UPGRADING.md entries a consuming app must work through to move from PormG version from up to to. The default to covers the current code — every released entry plus the uncut ## Unreleased changes the install is running (release-train model), so a consumer dev'ing PormG at HEAD sees work that has not been stamped with a release number yet. Pass to = pkgversion(PormG) to scope to the installed release only. Reads the UPGRADING.md shipped with the resolved PormG install, so the scope is accurate against the version your app actually depends on — not a latest-on-GitHub copy that may not match.

Entries print newest-first; each keeps its "How to find the calls to migrate" grep and its before → after, with the PormG-internal per-app rollout table trimmed off.

from is required — pass the PormG version your app currently depends on. Both from and to accept a VersionNumber or a version string (v"0.2" or "0.2").

Pass structured = true to get the entries back as data instead of printing — a Vector of (; version, title, body) named tuples, newest-first — for programmatic consumers.

Examples

julia> using PormG

julia> PormG.upgrade_guide(from = v"0.1")            # everything up to the installed version

julia> PormG.upgrade_guide(from = "0.2", to = "0.3")

julia> entries = PormG.upgrade_guide(from = v"0.1", structured = true);
source
PormG.@pormg_debugMacro
@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.KernelModule
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.BackendCapabilityErrorType
BackendCapabilityError(msg) <: PormGError

The active backend cannot do this — a PostgreSQL-only lookup on SQLite (JSONB containment, iunaccent_*), an explicit window frame= on SQLite, bulk_copy on SQLite, with_advisory_lock(...; on_missing_lock = :error) on SQLite, or a SQLite library older than a feature requires. The query is well-formed and the configuration is fine; the remedy is to change the request or the backend — each message names the specific way out. Split out of UnsupportedConnectionError in the pre-publish naming pass — capability limits are a user-facing contract, not an internal error.

source
PormG.Kernel.ConfigurationErrorType
ConfigurationError <: PormGError  (abstract)

Umbrella for connection-configuration failures — catch it to get every case below. Like FieldAccessError, this is an abstract mid-node rather than a throwable type, so the pre-existing MissingConfigurationError can live inside the bucket instead of beside it. catch ConfigurationError must not have holes; that class of surprise is the reason this taxonomy exists.

Subtypes: InvalidConfigurationError, WritesDisabledError (the change_data: false write switch — its remedy is a config edit), and Configuration.MissingConfigurationError (a missing folder/connection.yml, or a selected environment with no matching block).

source
PormG.Kernel.DatabaseErrorType
DatabaseError <: PormGError  (abstract)

Umbrella for failures raised by the database itself, once a statement has reached it — as opposed to the rest of the taxonomy, which reports misuse of PormG before anything is sent. catch DatabaseError covers every case below without naming a driver package.

Subtypes: IntegrityError (a constraint said no), OperationalError (transient — the connection dropped, a deadlock, a lock timeout), and StatementError (the statement itself was rejected, plus anything the backend could not classify).

All three are built from structured fields rather than a msg::String, so read them with error_message. Each keeps the driver's own exception in .cause, so SQLSTATE-level detail stays available to callers that want it:

try
    M.Driver.objects.create("code" => "SEN")
catch e
    e isa IntegrityError  && return conflict(error_message(e))
    e isa OperationalError && return retry()
    rethrow()
end

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

source
PormG.Kernel.DefinitionErrorType
DefinitionError <: PormGError  (abstract)

Umbrella for model-definition-time failures — catch DefinitionError covers both a bad field constructor argument (FieldValidationError) and a bad model/schema shape (ModelDefinitionError). They almost always surface together: one include("models.jl") can raise either, and a handler that names only one silently misses the other — which is exactly what UPGRADING.md's own #239 migration recipe did.

source
PormG.Kernel.DoesNotExistType
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.FieldValidationErrorType
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.FilterErrorType
FilterError(msg) <: PormGError

An invalid filter argument/shape, or an operator misused on a JSON/subquery column.

source
PormG.Kernel.IntegrityErrorType
IntegrityError(adapter, cause) <: 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. On PostgreSQL this is derived from SQLSTATE class 23, so it is exact; on SQLite it comes from SQLite's own literal constraint messages.

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

Connection configuration is present but unusable or inconsistent — an unsupported adapter, an unknown connection key, a malformed extensions setting, an unsupported PostgreSQL extension, a model not bound to a connection (or bound to an entry whose pool was never built), a missing driver package (using LibPQ / using SQLite forgotten), or an attempt to overwrite a static connection.

source
PormG.Kernel.InvalidMigrationErrorType
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, an unimplemented migrate_to(version) path, or a migration-engine step that cannot proceed (no pending plan, an unparseable introspected DDL statement, a missing model file). The importer-pointed-at-the- wrong-backend case is BackendCapabilityError.

source
PormG.Kernel.InvalidValueErrorType
InvalidValueError(msg) <: PormGError

A value failed coercion/type validation on insert/update, an identifier failed the fail-closed safety check, or an interval/duration literal could not be parsed. Also raised by the Models.format_*_sql coercion helpers, which the insert/update path calls (#239).

source
PormG.Kernel.LazyTraversalErrorType
LazyTraversalError(msg) <: FieldAccessError <: 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.MigrationErrorType
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.ModelDefinitionErrorType
ModelDefinitionError(msg) <: DefinitionError <: PormGError

A model or schema definition is invalid — more than one primary key, a duplicate related_name, an illegal field name, a UniqueConstraint that names an unknown or many-to-many field, an unresolvable ForeignKey / ManyToManyField target, or a Model(...) call given something that is not a PormGField.

source
PormG.Kernel.MultipleObjectsReturnedType
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.OperationalErrorType
OperationalError(adapter, cause) <: 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. PormG raises it for with_advisory_lock acquisition timeouts too: contention is a runtime condition, not misuse.

source
PormG.Kernel.PoolErrorType
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.PormGBackendType
PormGBackend <: PormGAbstractType

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

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

Deliberately not <: PormGSettings: that is the configuration/Settings type, and a pool carries none of its fields (#186).

Adding a backend means defining PormG.backend_* methods in a package extension — see Extending PormG.

source
PormG.Kernel.PormGBytesType
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:

  • PostgreSQLadd_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.
  • SQLiteadd_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.PormGErrorType
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 (FieldAccessError, PoolError, DatabaseError, ConfigurationError, MigrationError, DefinitionError) to group a family.

These are deliberately not <: ArgumentError: a clean break so callers match a type instead of string-matching a message. Plain Julia-level misuse (a missing kwarg, a missing path) still raises the stock Julia exception, because it is not a PormG domain error.

The type lives in PormG.Kernel — layer 1 — so every submodule can name it and its subtypes regardless of include order (#239). The full list is in the API reference.

source
PormG.Kernel.PormGFieldType
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). Values read back are not decoded through the field type.

See also PormG Field Types Reference.

source
PormG.Kernel.PormGModelType
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.ProtectedErrorType
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.QueryBuildErrorType
QueryBuildError(msg) <: 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.SQLObjectType
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.SQLObjectHandlerType
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).

See also SQLObject (the state it wraps).

source
PormG.Kernel.StatementErrorType
StatementError(adapter, cause) <: 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; the SQL text is deliberately not stored, because it can embed user data (the @error … sql=… log sites already surface the statement where that is appropriate).

The wording says could not execute, not the database rejected this, on purpose. Being the unclassified fallback means a PormG-internal fault on the statement path can land here too — the SQLite worker's malformed-payload invariant, for one — and claiming the server refused something it never saw would send a reader hunting for a SQL bug that does not exist.

source
PormG.Kernel.TransactionErrorType
TransactionError(msg) <: PormGError

The transaction API was used in a way that cannot work — atomic(durable=true) nested inside an open transaction, or an operation on a model bound to one connection attempted while a transaction is open on another.

Not a DatabaseError: nothing was sent, and the database is not involved. Both cases are caught before any statement is issued. A deadlock or a rollback the server forces is an OperationalError instead.

Introduced in #268 so the two checks stop reporting as unrelated types (QueryBuildError said "query shape" for what is a transaction-nesting mistake; InvalidConfigurationError said "your config is wrong" when the config was fine and the call pattern was not).

source
PormG.Kernel.UnknownFieldErrorType
UnknownFieldError(msg) <: FieldAccessError <: PormGError

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

source
PormG.Kernel.UnsupportedConnectionErrorType
UnsupportedConnectionError(msg) <: PormGError

A connection object that is neither a PostgreSQL nor a SQLite pool reached an execution path — a PormG internal dispatch bug; the message asks the user to report it. The catchable replacement for the internal ErrorException from #197.

Narrowed in the pre-publish naming pass: it previously also covered backend capability limits (now BackendCapabilityError) and models not bound to a connection (now InvalidConfigurationError, whose docstring always claimed that case) — three disjoint remedies distinguishable only by message text, which is the failure mode this taxonomy exists to remove.

source
PormG.Kernel.WritesDisabledErrorType
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_messageMethod
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 allDoesNotExist, 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.FunctionsModule
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.

AggregateSum, Avg, Count, Max, Min — see Filters and Aggregates

ConditionalCase, When — see Functions and Dates

WindowWindowOver, WindowSpec, Rank, DenseRank, RowNumber, Lag, Lead, FirstValue, LastValue, NthValue — see Window Functions

StringConcat, Lower, Upper, Length, Replace, Trim, LTrim, RTrim

MathAbs, Round, Floor, Ceil, Sqrt, Exp, Ln, Power, Mod

Type / valueCast, Extract, ToChar, Value, Coalesce, Greatest, Least, NullIf

They live here rather than at the top level because the names are generic enough to collide with Base and user code (Sum, Count, Max, Replace, Round, Length…), so the library has exactly one home and you opt in explicitly:

using PormG, PormG.Functions          # brings Sum, Count, … into scope
using PormG.Functions: Sum, Count     # …or just the ones you use
# or qualify without importing:
M.Result.objects.values("n" => PormG.Functions.Count("resultid"))

Q, Qor, F, Exists, OuterRef, Subquery, CTE and Interval are not part of this library — they are query primitives and stay on the top-level using PormG surface.

source
DataFrames.DataFrameMethod

Creates a DataFrame directly from a SQLObjectHandler query.

This extends the DataFrame constructor to work directly with PormG query objects,

Arguments

  • objct::SQLObjectHandler: The SQL object handler containing the query

Returns

  • DataFrames.DataFrame: The query results as a DataFrame

Example

query = M.Result |> object
query.filter("raceid__year" => 2020)
query.values("driverid__forename", "constructorid__name", "laps")
df = query |> DataFrame  # Direct conversion to DataFrame
source
PormG.QueryBuilder.IntervalType
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.ObjectHandlerType
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.PormGRowType

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

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.firstMethod
first(objct::SQLObjectHandler; show_query::Symbol = :execute)

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

Like every read terminal (count, exists, list, get), first executes on an internal copy of the handler — the limit(1) it needs is applied to that copy, never to objct. The handler is reusable afterwards, including for .update():

q = M.Driver.objects
q.filter("nationality" => "British")
driver = q.first()                      # q is unchanged — no limit leaks in
q.update("nationality" => "English")    # still valid on the same handler
source
Base.getMethod
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.

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

source
PormG.QueryBuilder.AvgMethod
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/MinAVG is covered by the to-many fan-out guard (#74): a join that multiplies rows would silently inflate the mean, so PormG raises instead. Passing distinct = true is an explicit opt-in and is exempt.

See also Filters and Aggregates.

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

Reference a column of a CTE declared with .with(...). The CTE's columns live in their own namespace, separate from the model's field paths, so a CTE may legally share a name with a field and neither shadows the other (#444):

parent_cte = M.Cj_parent.objects
parent_cte.values("id", "sku")

q = M.Cj_child.objects
q.with("parent" => parent_cte)          # "parent" is ALSO a ForeignKey of Cj_child
q.values("note",
         "parent__sku",                 # the ForeignKey's column — unambiguous
         "fk" => CTE("parent", "sku"))  # the CTE's column        — unambiguous

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.filter("raceid" => CTE("r91", "raceid"))                # correlate an unkeyed CTE (#44)
q.order_by(CTE("monaco_stats", "total_points"; desc = true))

An unaliased projection is named by joining the two with a double underscore — values(CTE("parent", "sku")) emits the output column parent__sku.

SQL functions, aggregates and window clauses accept a handle wherever they accept a field path — Lower(CTE("ev","sku")), Cast(CTE("ev","qty"), "text"), Sum(CTE("ev","qty")), Rank(over = WindowOver(partition_by = CTE("ev","sku"))).

desc = true is meaningful in order_by(...) and in a window's order_by; anywhere else it raises a QueryBuildError. A CTE column cannot be referenced from on(...), cjoin(...) or cjoin_on(...) — those clauses target model relations — however it is spelled, including as the operand of an F comparison.

See also Subqueries and CTEs.

source
PormG.QueryBuilder.CaseMethod
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.
  • output_field: the result type. Accepts a PormGField instance (e.g. CharField(), whose .type is used) or a raw SQL type string. Renders as a ::type cast on PostgreSQL and a CAST(...) on SQLite.

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

using PormG.Functions: Case, When
using PormG.Models: CharField          # field types are not part of PormG.Functions

"podium" => Case([
    When("positionorder" => 1, then = "win"),
    When("positionorder__@lte" => 3, then = "podium"),
], default = "none", output_field = CharField())

See also When, Functions and Dates.

source
PormG.QueryBuilder.CountMethod

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.DenseRankMethod
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.ExistsMethod
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))

SQLite returns 0/1 integers for a projected Exists; PostgreSQL returns booleans.

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

source
PormG.QueryBuilder.FMethod
F(field_name::String) -> 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 (* and / do not).

# Field-to-field comparison — grid position worse than finishing position
query = M.Result.objects.filter(F("grid") > F("positionorder"))

# Arithmetic projection, computed by the database
query = M.Result.objects
query.values("resultid", "adjusted" => F("points") * 2)

# Atomic update against the column's current value (no read-modify-write race)
M.Result.objects.filter("resultid" => 1).update("points" => F("points") + 1)

# Field-to-field update
M.Result.objects.filter("resultid" => 1).update("position" => F("positionorder"))

Prefer a plain lookup when the predicate compares against a scalar: write filter("points__@gt" => 20), not filter(F("points") > 20).

See also Field Expressions.

source
PormG.QueryBuilder.FirstValueMethod
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.LagMethod
Lag(x; offset::Integer = 1, default = nothing, over::WindowSpec = WindowOver())

Window LAG(x, offset) — the value of x from offset rows earlier in the window.

Arguments

  • x: the column to read. Required — 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 default are bound as query parameters, not interpolated.

See also Lead, Window Functions.

source
PormG.QueryBuilder.LastValueMethod
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 only
)

frame is PostgreSQL-only (BackendCapabilityError on SQLite). On SQLite, drop the order_by so the whole partition is one frame, or compute the value another way.

See also FirstValue, Window Functions.

source
PormG.QueryBuilder.LeadMethod
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.MaxMethod
Max(x)

Aggregate MAX(x) — the largest value of x in the group.

There is no distinct keyword: MAX(DISTINCT x) and MAX(x) are the same value.

MAX/MIN are deliberately exempt from the to-many fan-out guard (#74) that Count, Sum and Avg trip: duplicating rows across a to-many join cannot change an extremum, so the query is safe where a sum would be wrong.

The value comes back in whatever form the backend returns for that column — an aggregate is not decoded through the model field's type, so a MAX over a DateField is the driver's representation (a String on SQLite), not a Date. Convert it yourself if you need one.

See also Min, Filters and Aggregates.

source
PormG.QueryBuilder.MinMethod
Min(x)

Aggregate MIN(x) — the smallest value of x in the group. The mirror of Max in every respect: no distinct keyword, exempt from the fan-out guard (#74), and the result is the backend's own representation rather than the model field's type.

See also Filters and Aggregates.

source
PormG.QueryBuilder.NthValueMethod
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.OuterRefMethod
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.

Two limits, both enforced with a QueryBuildError:

  • One level only. It binds to the immediately enclosing query, so a projected subquery nested inside another projected subquery is rejected rather than silently correlated to the wrong level.
  • Correlated context required. Used outside an Exists/Subquery build there is no outer query to bind to.

Correlate on a base column of the outer model. A joined path (OuterRef("constructorid__name")) adds a join to the outer query and is outside the validated surface.

source
PormG.QueryBuilder.QMethod
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.QorMethod
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.RankMethod
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.RowNumberMethod
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.SubqueryMethod
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).

Outer `GROUP BY`

Combining a correlated Subquery with an outer aggregate is only well-defined when the correlated column is itself grouped. If it is not, PostgreSQL fails loudly while SQLite silently evaluates the subquery against an arbitrary row of each group. See Subqueries and CTEs.

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

source
PormG.QueryBuilder.ToCharMethod
ToChar(x, format::String; formatter = nothing)

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

Arguments

  • x: a field path, F expression, function object, or a vector of field paths.
  • format: a PostgreSQL to_char pattern, 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.
SQLite supports only the mapped formats

On SQLite the pattern is translated through PormG's strftime map rather than passed through, so only the patterns in that map work. The common date buckets ("YYYY", "YYYY-MM", "YYYY-MM-DD") are portable; exotic to_char patterns are PostgreSQL-only.

using PormG.Functions: ToChar, Count

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

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

See also Functions and Dates.

source
PormG.QueryBuilder.WhenMethod
When(condition; then = 0, otherwise = missing)

One WHEN condition THEN value branch of a SQL CASE.

condition accepts four forms:

  • a lookup pair — When("points__@gt" => 10, then = 1)
  • a tuple of pairs, ANDed together — When(("points__@gt" => 10, "grid" => 1), then = 1)
  • a Q(...) / Qor(...) object, for OR and nested boolean logic
  • an operator or function object, e.g. an F comparison

then defaults to 0. Both then and the CASE ELSE value are bound as query parameters.

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

See also Case, Functions and Dates.

source
PormG.QueryBuilder.WindowOverFunction
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.
  • frame: a raw frame clause such as "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING".

Both list arguments accept a bare scalar, so partition_by = "raceid" and partition_by = ["raceid"] are equivalent. An entry of any other type raises QueryBuildError.

`frame` is PostgreSQL-only

Passing frame on a SQLite connection raises BackendCapabilityError. Everything else here works on both backends (SQLite ≥ 3.25.0).

using PormG.Functions: WindowOver, Rank

# Rank drivers within each race — the ranking restarts per race.
query = M.Driver_standings.objects
query.filter("raceid__@in" => [305, 306], "points__@gt" => 0)
query.values(
    "raceid", "driverid__surname", "points",
    "race_rank" => Rank(over=WindowOver(
        partition_by=["raceid"],   # restart the ranking for each race
        order_by=["-points"]       # highest points = rank 1
    ))
)

See also Window Functions.

source
PormG.QueryBuilder.allocate_primary_keysMethod
allocate_primary_keys(objct::SQLObjectHandler, df::DataFrame; clone=true) -> DataFrame

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

Use this when you need the assigned ids before inserting—for example, to wire up foreign key columns in related tables that must be bulk-inserted in the same transaction.

If df already contains the primary key column with at least one non-blank value it is returned unchanged and no ids are reserved. If the column is absent, or every value in it is blank (missing, nothing, or an empty string), ids are reserved from the database.

If the column contains mixed values—some rows have explicit pk values and some are blank—a @warn is emitted and the DataFrame is still returned unchanged. The blank rows are left as-is and will raise a QueryBuildError when bulk_insert is called. To resolve this you can: (1) provide a pk value for every row, (2) remove the pk column so all ids are allocated automatically, or (3) pre-fill the blank rows before calling this function.

PostgreSQL

Uses nextval(pg_get_serial_sequence(...)) to atomically consume N values from the identity/serial sequence. The reserved ids are guaranteed not to collide with concurrent inserts. Note that if the subsequent bulk insert is never executed (e.g. the transaction is rolled back), the consumed sequence values are not returned—this is normal PostgreSQL sequence behaviour; gaps are harmless.

SQLite

Reads the starting point from max(MAX(pk), sqlite_sequence.seq), assigns the next N ids from there, and bumps the table's sqlite_sequence counter to the end of the reserved range. That keeps both later autoincrement inserts and later allocate_primary_keys() calls from reusing ids that were reserved but not yet inserted. This read-then-write is self-protecting: if it is not already running inside a transaction on this connection it auto-opens one (run_in_transaction, i.e. BEGIN IMMEDIATE + the in-process write lock) so no concurrent writer can claim the same range in between — mirroring bulk_insert/bulk_copy/bulk_update. Wrapping the whole pre-allocation and insert together in run_in_transaction is still recommended: then a rolled-back insert also releases the reserved ids, whereas a standalone allocation whose later insert fails durably burns its range (a harmless gap, exactly like PostgreSQL).

Arguments

  • objct: 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.
  • df: The DataFrame that will be bulk-inserted.
  • clone::Bool = true: When true (default) the returned DataFrame is a genuine copy with independent column vectors — the new pk column exists only on the returned frame, the caller's DataFrame is left untouched, and element writes on either frame never reach the other. Set to false to write the new pk column into the caller's DataFrame in place and skip the copy.

Notes

  • The returned pk column is a plain Vector{Int}. If the input DataFrame had 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_copyMethod
bulk_copy(objct::SQLObjectHandler, df_o::DataFrames.DataFrame; kwargs...)

Performs a high-speed bulk insert operation using PostgreSQL's COPY protocol. This is significantly faster than bulk_insert for large datasets.

Arguments

  • objct::SQLObjectHandler: The database handler object (e.g., M.Model).
  • df_o::DataFrames.DataFrame: The DataFrame containing the data to be inserted.
  • columns: (Optional) Specifies which columns to insert. Can be a String, a Pair{String, String}, or a Vector of these.
  • show_query::Bool = false: If true, prints the COPY command (note: data stream is not printed).

The caller's DataFrame is never mutated (and never copied — the pipeline works on a zero-copy wrapper).

The COPY protocol cannot express ON CONFLICT — rows that violate a unique constraint make the whole COPY fail. To skip or merge duplicates, use bulk_insert(...; on_conflict = ...) (#123).

Example

bulk_copy(M.Driver, df)
source
PormG.QueryBuilder.bulk_insertMethod

Inserts multiple rows into the database in bulk from a DataFrame.

Arguments

  • objct::SQLObjectHandler: The SQL object handler to use for the operation.
  • df_o::DataFrames.DataFrame: The DataFrame containing the data to be inserted.
  • columns: Optional. Specifies the columns to insert and their mappings. Can be nothing, a String, a Pair{String, String}, or a Vector of these. If nothing, all columns from the DataFrame are used.
  • chunk_size::Integer: Optional. The number of rows to insert in each batch (default: 1000).
  • show_query::Symbol: Optional. Return the generated SQL instead of executing it — :sql, :dict, :inspection, or :params (see Query Inspection). Defaults to :execute (run it).
  • on_conflict: Optional (#123). Attaches 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.

The caller's DataFrame is never mutated (and never copied — the pipeline works on a zero-copy wrapper), so there is no copy= knob to think about.

Examples

```julia include("models.jl") import models as mdl

Basic usage

query = mdl.User |> object df = DataFrame(name=["Alice", "Bob"], age=[30, 25]) bulk_insert(query, df)

With column mapping and excluding unwanted variables

query = mdl.Boook |> object df = DataFrame(title=["Book A", "Book B"], authorname=["Alice", "Bob"], year=[2020, 2021], ignoreme=["x", "y"])

Map DataFrame column "author_name" to model field "author"

Exclude "ignore_me" by not including it in the columns argument

bulkinsert(query, df, columns=["title", "year", "authorname" => "author"])

Only "title", "year", and "author_name" (as field "author") participate in the INSERT;

the DataFrame's columns are never renamed or removed — the mapping is internal.

```

source
PormG.QueryBuilder.bulk_updateMethod

Performs a bulk update operation on a database table using the provided DataFrame and a query object.

Arguments

  • objct::SQLObjectHandler: The database handler object.
  • df::DataFrames.DataFrame: The DataFrame containing the data to be used for the update.
  • columns: (Optional) The participating fields and their mappings — the single place a DataFrame column is mapped to a model field. Each entry is a String (DataFrame 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 DataFrame. 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 DataFrame 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 DataFrame column 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 DataFrame, 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::Bool: (Optional) If true, prints the generated SQL query. Defaults to false.
  • chunk_size::Integer: (Optional) Number of rows to process per chunk. Defaults to 1000.

The caller's DataFrame is never mutated (and never copied — the pipeline works on a zero-copy wrapper), so the operation is safe in asynchronous contexts without a copy= knob.

Example

# Update by primary key inferred from the DataFrame
bulk_update(objct, df)

# Set name/dof, matching rows on security_id
bulk_update(objct, df, columns=["name", "dof"], match_on=["security_id"])

# Map differing DataFrame names and add a constant scope guard. ALL df→field
# mappings live in columns=; match_on selects merge keys by model field name
# (a field listed in both is used only for matching — it is not SET).
bulk_update(objct, df,
    columns  = ["new_score" => "points",     # df "new_score" → field "points" (SET)
                "record_id" => "id"],        # df "record_id" → field "id" (match key)
    match_on = ["id"],                       # match Tb.id = source.id
    filters  = ["category_id" => 172100])    # constant: only this category
source
PormG.QueryBuilder.deleteMethod
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, PormGPostgres, PormGSQLite} = nothing: execute against this connection pool instead of the one on the model's settings (those two abstract types are the backend markers the concrete pools subtype). Only the pool is overridden; the settings, and therefore the dialect and 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.

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.

The collected statements then execute in dependency order inside a single transaction (BEGIN on PostgreSQL, BEGIN IMMEDIATE TRANSACTION on SQLite), so any failure rolls the whole set back.

Examples

# Delete objects from a model with a specific filter
query = M.Status.objects
query.filter("status" => "Engine")
total, dict = delete(query)

# Build the SQL without executing it
query = M.Just_a_test_deletion.objects
query.filter("test_result__constructorid__name" => "Williams")
sql = delete(query, show_query = :sql)

# Delete related tables (cascading delete)
query = M.Result.objects
query.filter("resultid" => 1)
total, dict = delete(query)

# Delete all objects from a model (use with caution)
query = M.Just_a_test_deletion.objects
total, dict = delete(query; allow_delete_all = true)

See also show_query, inspect_query, and Deleting Records.

source
PormG.QueryBuilder.deleteMethod
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.earliestMethod
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().

source
PormG.QueryBuilder.inspect_queryMethod
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 or :sqlite)
    • :model (String): The model/table name
    • :operation (Symbol): The query operation type (:select, :insert, :update, :delete)
    • :bucketing (Symbol): The parameter bucketing strategy (:numbered for PostgreSQL, :positional for SQLite)
    • :parameter_count (Int): Number of parameters
    • :parameter_buckets (Dict): Breakdown of parameters by bucket (for positional strategies)

Example

q = M.Driver.objects
q.filter("nationality" => "British")
q.order_by("surname")

inspection = q |> inspect_query()
# Dict with:
# :sql_text => "SELECT ... WHERE drivers.nationality = $1 ORDER BY ..."
# :parameters => ["British"]
# :dialect => :postgresql
# :model => "drivers"
# :operation => :select
# :bucketing => :numbered
source
PormG.QueryBuilder.latestMethod
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").

source
PormG.QueryBuilder.objectMethod
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)
  • .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
  • .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...) — add predicates to the ON clause of an existing join path
  • .cjoin("field" => "Model"; filters, join_type) — custom join at query time
  • .cjoin_on(model; alias, on, join_type) — anchor-less join where on is the entire ON clause
  • .with("name" => subquery; join_field, join_type) — define a CTE; call again for a second one
  • .select_for_update(; nowait, skip_locked, no_key)SELECT … FOR UPDATE row lock
  • .copy() — deep copy, to branch a chain without disturbing the original

Terminal methods

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

  • .list()Vector{PormGRow}; .list(:dict)Vector{Dict}; .list(:json) → JSON String
  • 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

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.pkMethod
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_sequencesMethod
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.saveMethod
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_queryFunction
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
:paramsVector — the parameterized values, in bucket order
:dict / :inspectionDict — the full metadata shape of inspect_query
: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 builds on a deepcopy, so it never mutates the query you pass (#43) — the same chain can be inspected and then executed.

For INSERT/UPDATE/DELETE, pass show_query= to the terminal method itself (query.delete(show_query = :sql)); this function always renders a SELECT. Use inspect_query when you want the metadata Dict with an explicit operation override.

source
PormG.Models.IndexType
Index(; fields, name = nothing)

Index a combination of columns — Django's Meta.indexes. Pass it to Model through indexes =.

An Index is a read-performance declaration, not a rule: it constrains nothing. For a composite uniqueness guarantee use UniqueConstraint, which is a CREATE UNIQUE INDEX and rejects duplicate rows.

fields names two or more fields on this model. The order is significant: an index over ("raceid", "lap") serves a lookup by raceid, or by raceid and lap together, but not one by lap alone. Foreign keys are referenced by their field name and resolved to the physical column (honoring db_column), and the declared case is preserved, so name each field exactly as it was declared.

One column is `db_index = true`, not a one-field `Index`

A single-column Index is rejected. It is not a missing feature — it is unrepresentable in both directions: a one-column CREATE INDEX is byte-identical whether db_index = true or an Index emitted it, so introspection reads it back as db_index (there is no marker to distinguish them, unlike UniqueConstraint, which SQLite tags origin = 'u' vs 'c'). A model declaring a one-field Index would therefore compare unequal to its own live table forever, and makemigrations would propose dropping the index on every run. Declare db_index = true on the field instead.

name is the index name. Omitted, the migration planner derives <table>_<cols>_idx, the plain sibling of the composite-unique convention. Pass an explicit one when the derived name would exceed PostgreSQL's 63-byte identifier limit, which Postgres truncates with only a NOTICE — truncation can collide two indexes into one.

Invalid declarations raise ModelDefinitionError as early as they can be detected: fewer than two fields, a repeated field, or a blank name fails here in the constructor; a field that does not exist on the model, a ManyToManyField (it owns no column), or two indexes sharing a name fail when the model is built. An index whose name collides with a UniqueConstraint's on the same table fails at migration planning, where both names are known.

Materialized when the table is created

Each index becomes a CREATE INDEX — identical on PostgreSQL and SQLite — emitted when its table is first created. Adding or removing one on a table that already exists is not yet detected by makemigrations, the same limitation UniqueConstraint carries. Introspection does read composite indexes back, so inspectdb on an existing database reproduces them. Declare an index with the model, or add it by hand on an existing table.

Examples

Lap_times = Models.Model("lap_times",
  raceid   = Models.ForeignKey(Race, pk_field = "raceid", on_delete = "CASCADE"),
  driverid = Models.ForeignKey(Driver, pk_field = "driverid", on_delete = "RESTRICT"),
  lap      = Models.IntegerField(),
  position = Models.IntegerField(),
  indexes = [
    Models.Index(fields = ("raceid", "lap"), name = "lap_times_race_lap_idx"),
  ],
)

which migrates to:

CREATE INDEX IF NOT EXISTS "lap_times_race_lap_idx"
  ON "lap_times" ("raceid", "lap");

See also Model, UniqueConstraint.

source
PormG.Models.UniqueConstraintType
UniqueConstraint(; fields, name = nothing)

Require a combination of columns to be unique together — Django's Meta.unique_together, spelled as a named constraint object. Pass it to Model through constraints =; a single-column rule is the field option unique = true instead.

fields names fields on this model — one name, or an iterable of them. Foreign keys are referenced by their field name and resolved to the physical column (honoring db_column), and the declared case is preserved, so name each field exactly as it was declared.

name is the index name. Omitted, the migration planner derives <table>_<cols>_uniq, matching the automatic many-to-many index convention. Pass an explicit one when the derived name would exceed PostgreSQL's 63-byte identifier limit, which Postgres silently truncates — truncation can collide two constraints into one index.

Invalid declarations raise ModelDefinitionError as early as they can be detected: no fields, a repeated field, or a blank name fails here in the constructor; a field that does not exist on the model, a ManyToManyField (it owns no column), or two constraints sharing a name fail when the model is built.

Materialized when the table is created

Each constraint becomes a CREATE UNIQUE INDEX — identical on PostgreSQL and SQLite — emitted when its table is first created. Adding or removing one on a table that already exists is not yet detected by makemigrations; it needs composite-index introspection PormG does not have (tracked as a follow-up). Declare composite uniqueness with the model, or add the index by hand.

Examples

Constructor_engine = Models.Model("constructor_engines",
  id                  = Models.IDField(),
  constructorid       = Models.ForeignKey(Constructor, pk_field = "constructorid", on_delete = "CASCADE"),
  year                = Models.IntegerField(),
  engine_manufacturer = Models.CharField(max_length = 50),
  constraints = [
    Models.UniqueConstraint(fields = ("constructorid", "year"), name = "uniq_constructor_year"),
  ],
)

which migrates to:

CREATE UNIQUE INDEX IF NOT EXISTS "uniq_constructor_year"
  ON "constructor_engines" ("constructorid", "year");

See also Model.

source
PormG.Models.AutoFieldMethod
AutoField(; kwargs...)

Retired (#408). Use IDField. Calling this raises FieldValidationError.

AutoField was documented as a 32-bit auto-incrementing integer primary key — "INTEGER with SERIAL auto-increment". It never was one. Dialect._get_column_type had no sAutoField branch on either backend, so the field fell through to the else and emitted a TEXT column, on PostgreSQL and SQLite alike, with no sequence, identity, or AUTOINCREMENT behind it. A model keyed on it produced a text primary key that nothing could allocate, and makemigrations could never converge, because what the field declared and what introspection read back could not agree.

It is retired rather than repaired because repairing it buys almost nothing and costs a whole class of defect (#409). IDField is the only integer key type PormG's introspection reads back, so any other one is condemned to a perpetual ALTER on every makemigrations. On SQLite the distinction is not even physical: INTEGER PRIMARY KEY is a 64-bit rowid alias, so an AutoField column and an IDField column are byte-identical. The saving was four bytes per row, on one backend, for a type that had never worked — and the Django importer had already been routed away from it for exactly these reasons (#399, DJANGO_AUTO_KEY_TYPES).

This stub exists so a consuming app fails at the declaration with an actionable message instead of an UndefVarError from a generated models file. It is a pre-publish migration aid and is removed before the first General-registry release; see UPGRADING.md.

Migration

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

# after
Part_category = Models.Model(
    id   = Models.IDField(),
    name = Models.CharField(max_length = 100)
)

IDField is BIGINT rather than INTEGER, but an existing PostgreSQL table whose key really is integer keeps that column: PormG compares the field's declared type slot, not the rendered width. Two caveats, both covered in UPGRADING.md — a key that is not an IDENTITY column still attracts an ADD GENERATED BY DEFAULT AS IDENTITY (a pre-existing :generated mismatch), and a column a real AutoField created is text. PostgreSQL refuses that as an identity column, so the migration errors; SQLite does not refuse it — it rebuilds the table into INTEGER PRIMARY KEY AUTOINCREMENT, which aborts on a non-numeric key and silently renumbers a zero-padded one ('0042' becomes 42). Re-type such a column by hand.

source
PormG.Models.BigIntegerFieldMethod
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
  • 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.Model(
    id = IDField(),
    user_id = BigIntegerField(db_index=true),
    session_id = BigIntegerField(),
    timestamp_ms = BigIntegerField()  # Unix timestamp in milliseconds
)

Population and statistics:

Country = Models.Model(
    id = IDField(),
    name = CharField(max_length=100),
    population = BigIntegerField(null=true),
    gdp_usd = BigIntegerField(null=true),  # GDP in USD cents
    area_sq_meters = BigIntegerField()
)

Large external identifiers:

SocialMedia = Models.Model(
    id = IDField(),
    user = ForeignKey("User"),
    twitter_id = BigIntegerField(unique=true, null=true),
    facebook_id = BigIntegerField(unique=true, null=true),
    follower_count = BigIntegerField(default=0)
)

Common Use Cases

  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.BinaryFieldMethod
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.
  • default::Union{Vector{UInt8}, Nothing} = nothing: rendered into the DDL as a byte literal ('\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.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.BooleanFieldMethod
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)
  • editable::Bool = false: Whether the field should be editable in forms

Examples

Basic boolean flags:

User = Models.Model(
    id = IDField(),
    username = CharField(max_length=150),
    is_active = BooleanField(default=true),
    is_staff = BooleanField(default=false),
    email_verified = BooleanField(default=false)
)

Boolean Values and Conversion

The field handles various input formats:

  • Julia Bool: true, false
  • Integers: 1 (true), 0 (false)
  • Strings: "true", "false", "1", "0", "yes", "no"
  • NULL: When null=true, accepts NULL/nothing
source
PormG.Models.CharFieldMethod
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
  • 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.Model(
    id = IDField(),
    username = CharField(max_length=150, unique=true),
    first_name = CharField(max_length=50),
    last_name = CharField(max_length=50)
)

String field with choices (enumeration):

Order = Models.Model(
    id = IDField()
    status = CharField(
        max_length=20,
        choices=(
            ("1", "Pending"),
            ("2", "Processing"),
            ("3", "Shipped"),
            ("4", "Delivered"),
            ("5", "Cancelled")
        ),
        default="1"
    )
    customer_name = CharField(max_length=200)
)

Field with a human-readable label (the column name follows the field name, "sku"):

Product = Models.Model(
    id = IDField(),
    name = CharField(max_length=200),
    sku = CharField(
        max_length=50,
        unique=true,
        verbose_name="Stock Keeping Unit"
    )
    category = CharField(max_length=100, null=true, blank=true)
)

Indexed field for performance:

Article = Models.Model(
    id = IDField(),
    title = CharField(max_length=200, db_index=true),
    slug = CharField(max_length=200, unique=true, db_index=true),
    content = TextField()
)

Choices Feature

The choices parameter allows you to restrict field values to a predefined set:

# Define choices as tuples of (value, display_name)
priority_choices = (
    ("low", "Low Priority"),
    ("medium", "Medium Priority"),
    ("high", "High Priority"),
    ("urgent", "Urgent")
)

Task = Models.Model(
    id = IDField(),
    title = CharField(max_length=200),
    priority = CharField(max_length=10, choices=priority_choices, default="medium")
)

Choice Format Options:

  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: rename the field — the column follows the field name; db_column is not currently honored by schema generation

Notes

  • The field uses VARCHAR type which is efficient for short to medium strings
  • Choices are validated at the Julia application level, not in the database
  • The editable=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.DateFieldMethod
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{String, Nothing} = nothing: Default value for the field (YYYY-MM-DD format)
  • 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.Model(
    id = IDField(),
    username = CharField(max_length=150),
    birth_date = DateField(null=true, blank=true),
    join_date = DateField(auto_now_add=true),
    last_login_date = DateField(null=true)
)

Event and scheduling:

Event = Models.Model(
    id = IDField(),
    title = CharField(max_length=200),
    event_date = DateField(db_index=true),
    registration_deadline = DateField(),
    created_date = DateField(auto_now_add=true)
)

Business dates:

Invoice = Models.Model(
    id = IDField(),
    customer = ForeignKey("Customer"),
    issue_date = DateField(auto_now_add=true),
    due_date = DateField(),
    paid_date = DateField(null=true, blank=true)
)

Auto Date Features

autonowadd

Sets the date automatically when the record is first created:

created_date = DateField(auto_now_add=true)
# Automatically set to today's date on creation
# Never changes after initial 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

The field accepts various input formats:

  • Julia Date: Date(2024, 7, 28)
  • DateTime: DateTime(2024, 7, 28, 10, 30) (time ignored)
  • String ISO: "2024-07-28"
  • String formats: Various date strings parseable by Julia
source
PormG.Models.DateTimeFieldMethod
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
  • 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.DecimalFieldMethod
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
  • 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
  • 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.DurationFieldMethod
DurationField(; kwargs...)

An elapsed time span — INTERVAL on both PostgreSQL and SQLite.

default is validated at model-definition time and re-raised as FieldValidationError, so a bad default= surfaces where the mistake is rather than on the insert path (where the same coercion raises InvalidValueError).

Examples

Pit_task = Models.Model("pit_task",
  id                 = Models.IDField(),
  name               = Models.CharField(max_length = 200),
  estimated_duration = Models.DurationField(),
  actual_duration    = Models.DurationField(null = true),
)

See also TimeField, DateTimeField.

source
PormG.Models.EmailFieldMethod
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
  • 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.FileFieldMethod
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.FloatFieldMethod
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
  • 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.ForeignKeyMethod
ForeignKey(to::Union{String, PormGModel}; kwargs...)

A field that creates a many-to-one relationship to another model, similar to Django's ForeignKey.

The ForeignKey field represents a relationship where many records in the current model can reference a single record in the target model. It creates a foreign key constraint in the database and enables efficient querying of related data.

Required Arguments

  • to::Union{String, PormGModel}: The target model that this field references. Can be either:
    • A string with the model name (e.g., "User", "Category")
    • A direct reference to a PormGModel instance

Keyword Arguments

  • verbose_name::Union{String, Nothing} = nothing: A human-readable name for the 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)
  • 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
  • on_update::Union{String, Nothing} = nothing: Action when the referenced object's key is updated
  • deferrable::Bool = false: Whether the constraint check can be deferred until transaction commit
  • initially_deferred::Bool = false: Whether constraint checking is initially deferred
  • 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.Model(
    id = IDField()
    title = CharField(max_length=200)
    author = ForeignKey("User")
    category = ForeignKey("Category", on_delete=CASCADE)
)

Foreign key allowing NULL values:

Product = Models.Model(
    id = IDField()
    name = CharField(max_length=100)
    category = ForeignKey("Category", null=true, blank=true, on_delete=SET_NULL)
)

Multiple foreign keys to the same model — related_name is optional, but recommended:

Message = Models.Model(
    id = IDField()
    sender = ForeignKey("User", related_name="sent_messages")
    recipient = ForeignKey("User", related_name="received_messages")
    content = TextField()
)

Related Names and Reverse Relations

  • If related_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
  • Invalid parameters trigger warnings but don't cause errors

Notes

  • The field stores the primary key value of the referenced object
  • Uses BIGINT type to match IDField primary keys
  • Supports deferred constraint checking for complex transactions
  • Compatible with PostgreSQL's foreign key features

See Also

  • Django's ForeignKey documentation for conceptual understanding
source
PormG.Models.IDFieldMethod
IDField(; kwargs...)

A field type for auto-incrementing integer primary keys, equivalent to PostgreSQL's BIGSERIAL or GENERATED AS IDENTITY columns.

The IDField is typically used as the primary key for models and automatically generates unique integer values for each record. It maps to a PostgreSQL BIGINT column with auto-increment capabilities.

Keyword Arguments

  • verbose_name::Union{String, Nothing} = nothing: A human-readable name for the field
  • primary_key::Bool = true: Whether this field is the primary key for the table
  • auto_increment::Bool = true: Whether the field should auto-increment (generate values automatically)
  • unique::Bool = true: Whether values in this field must be unique across all 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 auto-increment)
  • editable::Bool = false: Whether the field should be editable in forms (typically false for ID fields)
  • generated::Bool = true: Whether to use PostgreSQL's GENERATED AS IDENTITY feature
  • generated_always::Bool = false: Whether to use GENERATED ALWAYS AS IDENTITY (stricter than regular GENERATED)

Database Mapping

  • PostgreSQL Type: BIGINT with GENERATED AS IDENTITY or GENERATED ALWAYS AS IDENTITY
  • Auto-increment: Supported through PostgreSQL's identity columns
  • Index: Automatically indexed as primary key

Examples

Basic usage (most common):

User = Models.Model(
    id::PormGField = IDField()
    name::PormGField = CharField(max_length=100)
    email::PormGField = EmailField()
)

Using GENERATED ALWAYS (stricter identity):

Order = Models.Model(
    id::PormGField = IDField(generated_always=true)
    customer_id::PormGField = ForeignKey("Customer")
    order_date::PormGField = DateTimeField()
)

Notes

  • The IDField is designed to be the primary key and should typically be the first field in your model
  • Values are automatically generated by the database, so you don't need to provide them when creating records
  • The field uses BIGINT type to support large ranges of ID values
  • When generated_always=true, the database will reject any attempts to manually insert ID values
  • This field type is PostgreSQL-specific and optimized for PormG's PostgreSQL backend

Validation

  • All boolean parameters are validated to ensure type safety
  • The verbose_name must be a String or nothing
  • The default value, if provided, must be convertible to Int64
  • Invalid parameters will trigger warnings but won't cause errors (they'll be ignored)
source
PormG.Models.ImageFieldMethod
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
  • 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.IntegerFieldMethod
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
  • editable::Bool = false: Whether the field should be editable in forms

Examples

Basic integer field:

Product = Models.Model(
    id = IDField(),
    name = CharField(max_length=200),
    quantity = IntegerField(default=0),
    price_cents = IntegerField()  # Store price in cents to avoid decimals
)

Integer field with constraints:

User = Models.Model(
    id = IDField(),
    username = CharField(max_length=150, unique=true),
    age = IntegerField(null=true, blank=true),
    score = IntegerField(default=0, db_index=true)
)

Rating system:

Review = Models.Model(
    id = IDField(),
    product = ForeignKey("Product"),
    rating = IntegerField(default=5),  # 1-5 star rating
    helpful_votes = IntegerField(default=0)
)

Validation and Constraints

  • Range: Automatically validates within INTEGER bounds
  • Type: Accepts integers, numeric strings (converted automatically)
  • Default: Must be an integer or convertible to integer
  • Null: When null=true, accepts NULL values

Migration Considerations

  • Range Changes: Changing to BigIntegerField is safe
  • Adding Constraints: Adding uniqueness or indexes is safe
  • Default Values: Can be added or modified safely
  • Null Constraints: Removing null constraint requires data validation
source
PormG.Models.JSONFieldMethod
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
  • editable::Bool = true: Whether the field should be editable in forms

Database Mapping

  • PostgreSQL Type: JSONB
  • SQLite Type: TEXT

Examples

Race = Models.Model(
    id = IDField(),
    name = CharField(max_length=200),
    metadata = JSONField(null=true, blank=true, verbose_name="Extra Data")
)

Notes

  • Values must be valid JSON strings when passed as strings.
  • Dict and Vector values are automatically serialized to JSON strings.
  • PostgreSQL JSONB supports GIN indexing for efficient key/value lookups.
source
PormG.Models.ManyToManyFieldMethod
ManyToManyField(to::Union{String, PormGModel}; kwargs...)

Declare a many-to-many relationship without adding a physical column to the owning model table. When through is omitted, migrations synthesize a join table with two foreign keys and a composite unique index.

Keyword Arguments

  • through::Union{String, PormGModel, Nothing} = nothing: explicit through model; skips auto table synthesis.
  • related_name::Union{String, Nothing} = nothing: reverse accessor on the target model. Omitted, it is derived like 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.ModelMethod
Model(; constraints = nothing, db_table = nothing, indexes = nothing, fields...)
Model(name; constraints = nothing, db_table = nothing, indexes = nothing, fields...)

Define a model — one database table, described by its fields. Returns a Model_Type: the object the query builder starts from, as in M.Driver.objects.

Every keyword other than constraints/db_table/indexes declares a field: field_name = FieldType(...), where the field types are the constructors in this module (IDField, CharField, ForeignKey, …). Declaring a keyword whose value is not a field raises ModelDefinitionError — see the note below, which is the usual reason that happens.

The table name comes from the positional string when given, and otherwise from the Julia binding the model is assigned to, filled in when set_models (or @import_models) registers the module. Both forms are idiomatic and Race = Model(...) and Race = Model("race", ...) name the same table, because a binding-derived name is lowercased as it is filled in. Reach for the positional form when the binding name is not the table name you want — and db_table (below) when even that isn't enough, because the physical table isn't a valid PormG model name at all.

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. db_table (#59) pins an explicit physical table name, preserved verbatim — no case fold, no validation beyond "is it a non-empty String" — overriding the name otherwise derived from the positional argument or the binding. It is authoritative everywhere a table identifier is rendered: DDL, SELECT/INSERT/UPDATE/DELETE, JOIN, foreign-key REFERENCES targets, and migration diffing. Unset (the default), a model behaves exactly as it did before this option existed. This is the table-level sibling of CharField's db_column (#50):

# The logical name stays lowercase; db_table carries the exact legacy spelling.
DriverRaces = Models.Model("driver_races", db_table = "Driver_Races_Legacy",
  id = Models.IDField(),
)

indexes (#347) takes Index objects — one, or a collection — for a read-performance index spanning more than one column, Django's Meta.indexes. A single-column index stays the field option db_index = true.

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

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

All three are peeled off before the field keywords, so db_table = CharField() declares the option (and raises, since a field is not a String) rather than a column called db_table. To declare a column with one of those names, pin it with db_column:

table_kind = Models.CharField(db_column = "db_table")

var"db_table" = CharField() does not help: it parses to the keyword-argument name :db_table, and the peel keys on that name however it was spelled. db_column is the only spelling that genuinely declares the column. A table named db_table needs nothing special — Model("db_table", …) is fine.

PormG has no Django `Meta` block

There is no model-level ordering or verbose_namedb_table (above) is the one model-level physical-naming option. Any other keyword is read as a field declaration and raises:

Models.Model("race", ordering = ["-year"], raceid = Models.IDField())
# ModelDefinitionError: All fields must be of type PormGField, exemple: …

Instead: order at query time with order_by() — there is no per-model default sort; and set verbose_name per field, where it is accepted, rather than per model.

Examples

Circuit = Models.Model(                       # table `circuit`, inferred from the binding
  circuitid = Models.IDField(),
  name      = Models.CharField(max_length = 100),
  country   = Models.CharField(max_length = 50),
)

Race = Models.Model(
  raceid    = Models.IDField(),
  year      = Models.IntegerField(),
  round     = Models.IntegerField(),
  circuitid = Models.ForeignKey(Circuit, pk_field = "circuitid", on_delete = "CASCADE"),
  date      = Models.DateField(),
  time      = Models.TimeField(null = true),
  constraints = [                             # no two races share a (year, round)
    Models.UniqueConstraint(fields = ("year", "round"), name = "race_year_round_uniq"),
  ],
)

Lap_times = Models.Model(
  raceid   = Models.ForeignKey(Race, pk_field = "raceid", on_delete = "CASCADE"),
  driverid = Models.ForeignKey(Driver, pk_field = "driverid", on_delete = "RESTRICT"),
  lap      = Models.IntegerField(),
  position = Models.IntegerField(),
  indexes = [                                 # many rows per (race, lap) — an index, not a rule
    Models.Index(fields = ("raceid", "lap"), name = "lap_times_race_lap_idx"),
  ],
)

# Porting a legacy schema PormG doesn't control: the live table is `Constructor_Standings`.
ConstructorStandings = Models.Model("constructor_standings",
  db_table = "Constructor_Standings",
  id = Models.IDField(),
)

See also set_models, UniqueConstraint, Index, ForeignKey.

source
PormG.Models.OneToOneFieldMethod
OneToOneField(to::Union{String, PormGModel}; kwargs...)

A field that creates a one-to-one relationship to another model, similar to Django's OneToOneField.

The OneToOneField represents a strict one-to-one relationship where each record in the current model corresponds to exactly one record in the target model, and vice versa. It's essentially a ForeignKey with a unique constraint that ensures no two records can reference the same target record.

Required Arguments

  • to::Union{String, PormGModel}: The target model that this field references. Can be either:
    • A string with the model name (e.g., "UserProfile", "Settings")
    • A direct reference to a PormGModel instance

Keyword Arguments

  • verbose_name::Union{String, Nothing} = nothing: A human-readable name for the 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)
  • 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
  • on_update::Union{String, Nothing} = nothing: Action when the referenced object's key is updated
  • deferrable::Bool = false: Whether the constraint check can be deferred until transaction commit
  • initially_deferred::Bool = false: Whether constraint checking is initially deferred
  • 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.Model(
    id = IDField()
    username = CharField(max_length=150, unique=true)
    email = EmailField()
)

UserProfile = Models.Model(
    id = IDField()
    user = OneToOneField("User", on_delete=CASCADE)
    bio = TextField(blank=true)
    avatar = ImageField(blank=true)
    birth_date = DateField(null=true, blank=true)
)

One-to-one with null values allowed:

Employee = Models.Model(
    id = IDField()
    name = CharField(max_length=100)
    department = CharField(max_length=50)
)

EmployeeSettings = Models.Model(
    id = IDField()
    employee = OneToOneField("Employee", null=true, blank=true, on_delete=SET_NULL)
    email_notifications = BooleanField(default=true)
    theme_preference = CharField(max_length=20, default="light")
)

Extending a model without modifying it:

Product = Models.Model(
    id = IDField()
    name = CharField(max_length=200)
    price = DecimalField(max_digits=10, decimal_places=2)
)

ProductDetails = Models.Model(
    id = IDField()
    product = OneToOneField("Product", on_delete=CASCADE, related_name="details")
    detailed_description = TextField()
    technical_specs = TextField()
    warranty_info = TextField()
)

Database Constraints vs. Unique ForeignKey

The two spellings produce the same physical column:

# The same column: the referenced key's type, UNIQUE, plus the foreign-key constraint.
user = OneToOneField("User")
user = ForeignKey("User", unique=true)

OneToOneField is nonetheless the spelling to declare, and not only for readability: it is what introspection reports for such a column, on both PostgreSQL and SQLite (#417). Declaring ForeignKey(..., unique=true) leaves the models file naming a different struct than the live schema reads back, and while the two compare equal attribute-for-attribute, the migration planner falls back to a struct-type comparison as soon as any other column in that table changes — which proposes an ALTER that re-renders the column unchanged (a full table rebuild on SQLite).

Validation

  • The 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
  • Invalid parameters trigger warnings but don't cause errors

See Also

  • Django's OneToOneField documentation for conceptual understanding
  • Database normalization principles for when to use one-to-one relationships
source
PormG.Models.PasswordFieldMethod
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.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.PositiveIntegerFieldMethod
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)
  • editable::Bool = false: Whether the field should be editable in forms

Database Mapping

  • PostgreSQL Type: INTEGER + CHECK ("col" >= 0)
  • SQLite Type: INTEGER UNSIGNED (INTEGER affinity) + CHECK ("col" >= 0)
  • Range: 0 to 2147483647

Examples

Lap_times = Models.Model(
    id = IDField(),
    lap = PositiveIntegerField(default=1),
    milliseconds = PositiveIntegerField()
)
source
PormG.Models.PositiveSmallIntegerFieldMethod
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)
  • editable::Bool = false: Whether the field should be editable in forms

Database Mapping

  • PostgreSQL Type: SMALLINT + CHECK ("col" >= 0)
  • SQLite Type: SMALLINT (INTEGER affinity) + CHECK ("col" >= 0)
  • Range: 0 to 32767

Examples

Standing = Models.Model(
    id = IDField(),
    position = PositiveSmallIntegerField(default=1),
    points = PositiveSmallIntegerField(default=0)
)
source
PormG.Models.SlugFieldMethod
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
  • 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.Model(
    id = IDField(),
    name = CharField(max_length=200),
    slug = SlugField(unique=true, verbose_name="URL Slug")
)
source
PormG.Models.TextFieldMethod
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
  • 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.TimeFieldMethod
TimeField(; kwargs...)

Time of day with no date component — SQL TIME.

default accepts a Time or anything Time(x) parses (e.g. "09:30:00"); an invalid value raises FieldValidationError at model-definition time rather than on the first insert.

Examples

Team_store = Models.Model("team_store",
  id           = Models.IDField(),
  name         = Models.CharField(max_length = 100),
  opening_time = Models.TimeField(),
  closing_time = Models.TimeField(null = true),
)

See also DateField, DateTimeField, DurationField.

source
PormG.Models.URLFieldMethod
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
  • 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.Model(
    id = IDField(),
    name = CharField(max_length=200),
    wiki_url = URLField(null=true, blank=true, verbose_name="Wikipedia Link")
)
source
PormG.Models.UUIDFieldMethod
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
  • 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.Model(
    id = IDField(),
    session_token = UUIDField(unique=true, db_index=true),
    user_id = ForeignKey("User")
)
source
PormG.Models.set_modelsMethod
set_models(_module::Module, path::String) -> nothing

Register every model defined in _module against the database configuration folder path, and resolve the relationships between them. This is the manual model-loading path; prefer PormG.@import_models (or PormG.@models_module for inline definitions), which call it for you and also handle precompilation and Revise reloads.

Until a model is registered it is inert: it has no connection and no reverse relations. Building a query still appears to work — M.Driver.objects.filter(...) returns a handler — but rendering or executing it raises InvalidConfigurationError.

Registration does four things:

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

  2. Binds the connection. path is matched against the configured folders to find the connection key. A folder that is not loaded yet is loaded implicitly — which also fixes the environment, so call PormG.Configuration.load(path; env = ...) first when you need a specific one, and expect MissingConfigurationError from that implicit load if path holds no connection.yml.

  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__ that @import_models and @models_module inject, and the Revise reload callback, all catch — though the first load of either macro still surfaces the error. delete() keeps its own copy of both checks, and that is what still raises for a contradiction the caller never saw.

Calling it again is safe and is the supported way to pick up edits: reverse relations and many-to-many caches are cleared before being rebuilt, so a reload cannot accumulate duplicates.

Examples

module f1_models
import PormG.Models

Circuit = Models.Model(
  circuitid = Models.IDField(),
  name      = Models.CharField(max_length = 100),
)

Race = Models.Model(
  raceid    = Models.IDField(),
  year      = Models.IntegerField(),
  circuitid = Models.ForeignKey(Circuit, pk_field = "circuitid", on_delete = "CASCADE"),
)

Models.set_models(@__MODULE__, "db")   # tables `circuit` / `race`; Circuit gains a `race` accessor
end

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

source
PormG.Migrations.MIGRATION_FORMAT_VERSIONConstant
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.DestructiveMigrationErrorType
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.DryRunResultType
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.

source
PormG.Migrations.convertSQLToModelMethod

convertSQLToModel(sql::String)

Converts a SQL CREATE TABLE statement into a model definition in PormGModel.

Arguments

  • sql::String: The SQL CREATE TABLE statement.

Returns

  • PormGModel: The model definition.

Example

source
PormG.Migrations.convert_schema_to_modelsMethod

convertschematomodels(db::PormGPostgres; ignoretable::Vector{String} = postgresignoretable)

Convert the database schema to models.

Arguments

  • db::PormGPostgres: The database connection.
  • ignore_table::Vector{String}: A vector of table names to ignore. Defaults to postgres_ignore_table.

Returns

  • models_array::Vector{Any}: A vector containing the converted models.

Description

This function retrieves the database schema and converts it to models. It collects all create instructions and skips tables specified in the ignore_table vector. The function prints the type of each schema and returns the schema for debugging purposes. It stops processing after the fifth schema.

source
PormG.Migrations.discard_pending_migrationMethod
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 remove_migration_record / migrate_to, which mutate applied state). That makes discarding a draft the one inherently safe, reversible migration op.

When backup=true (default) the file is renamed to pending_migrations.jl.discarded (overwriting any previous discard) so the draft can be recovered; otherwise it is deleted. makemigrations overwrites the pending file anyway, so a later regenerate is unaffected.

Returns (discarded=true, path, backup, tables, statements) describing what was thrown away, or nothing when there is no pending migration. Pairs with status, which reports whether a pending file exists.

source
PormG.Migrations.django_to_stringMethod
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_runMethod
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.

source
PormG.Migrations.get_migration_planMethod
get_migration_plan(models, current_schema, conn, settings; interactive = true)

Diff the model definitions against the live database schema and return the DDL that would reconcile them, as an OrderedDict{Symbol, OrderedDict{String, String}} — model name ⇒ ordered (human description ⇒ SQL statement). It only computes the plan; nothing is written or executed. makemigrations is the entry point that drives it.

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.

An empty models means an empty database, so every model becomes a CREATE TABLE.

With interactive = true (the default) a model with no matching table prompts whether it is new or a rename of a table that disappeared, so a rename keeps its data. interactive = false answers "new table" and "not a rename" for everything — a non-interactive run therefore never renames, it drops and creates. Choosing a nonexistent option at the prompt raises InvalidMigrationError.

source
PormG.Migrations.import_models_from_djangoMethod

importmodelsfromdjango(apps::AbstractVector{<:Pair}; db::String = DBPATH, forcereplace::Bool = false, file::String = "automaticmodels.jl", outputpath::Union{Nothing, String} = nothing, authusermodel::Union{Nothing, String} = nothing, strictrelations::Bool = false, strictfields::Bool = false, bindingoverrides::AbstractDict = Dict{String, String}(), autofieldsignore::Vector{String} = ["Manager"], parametersignore::Vector{String} = ["help_text"])

Import a multi-app Django project — "<app_label>" => "<models.py path or source>" pairs — into one generated module.

import_models_from_django(
  ["core"    => "server/core/models.py",
   "access"  => "server/access/models.py",
   "imports" => "server/imports/models.py"];
  db = "sgrh", file = "models.jl", force_replace = true)

Why one module and not one per app

A Django project with N apps is ONE database, and PormG is structurally one models file per connection: makemigrations/migrate resolve a single joinpath(db, settings.model_file) and load it into a single module. One module per app would not merely be unsupported — it would be invisible to the migration engine. Emitting every app into one module also makes every cross-app foreign key a same-module binding, which is the only thing _resolve_target_model can resolve.

Each model's physical table is <app_label>_<lowercased class name> (Django's own derivation), pinned as db_table, so one file carries every app's tables. A Meta.db_table still overrides it.

Class-name collisions

When two apps declare the same class name, both are renamed to <app>_<class> — binding Core_pessoa and Access_pessoa, never Pessoa and Pessoa2. Renaming only the second would make the output depend on the order the apps were listed, and set_models keys reverse accessors on the logical name, so one model would answer to pessoa and the other to pessoa2. The rename is lossless because db_table carries the real table either way. Use binding_overrides to choose a different spelling.

Arguments

Every keyword of the single-app method applies, except django_prefix: the app label now comes from each pair. A django_prefix on the resolved config is rejected, not ignored — get_model_name would strip that one prefix from every logical name, so core_pessoa would become pessoa while access_pessoa survived intact, and the reverse lookup would then want Pessoa while the binding is Core_pessoa. Cheap to detect here; near-impossible to diagnose at query time.

Relation targets

"self", "<app_label>.<ClassName>", a bare "<ClassName>" (same app first, then a globally unique one), and settings.AUTH_USER_MODEL all resolve. A target outside the imported app set keeps its column and loses the relation, with a # PormG: marker — see strict_relations.

See also import_models_from_django(::String), django_to_string.

source
PormG.Migrations.import_models_from_djangoMethod

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

  • 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_gal") 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. "estoque") 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 Dim_ibge under django_prefix = "estoque" emits Dim_ibge = Models.Model("dim_ibge", db_table = "estoque_dim_ibge", …). 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 estoque_dim_ibge_ufs 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 (GenericIPAddressField, 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_gal/) with its own folder and prefix:

importmodelsfromdjango("dbgal/models.py"; db="db", file="galmodels.jl", outputpath="dbgal", djangoprefix="estoque", force_replace=true)

source
PormG.Migrations.import_models_from_postgresMethod
import_models_from_postgres(db::String; force_replace::Bool=false, ignore_table::Vector{String}=postgres_ignore_table, file::String="automatic_models.jl")

Import models from a PostgreSQL database and generate a Julia file with model definitions.

Arguments

  • db::String: The database key from the configuration.
  • force_replace::Bool=false: Whether to overwrite the file if it already exists.
  • ignore_table::Vector{String}=postgres_ignore_table: A vector of table name patterns to ignore.
  • file::String="automatic_models.jl": The output filename for the generated models.

Description

This function retrieves the database schema from PostgreSQL, converts each table to a PormG model, and generates a Julia module file containing all the model definitions. The generated file can be directly included in your project to work with the database tables.

Example

using PormG

# Load the database configuration
PormG.Configuration.load("db")

# Import models from the database
PormG.Migrations.import_models_from_postgres("db")

# Or with options
PormG.Migrations.import_models_from_postgres("db", force_replace=true, file="my_models.jl")
source
PormG.Migrations.import_models_from_sqliteFunction
import_models_from_sqlite(db::String="db"; force_replace::Bool=false, ignore_schema::Vector{String}=sqlite_ignore_schema, include_table=nothing, file::String="automatic_models.jl")

Import models from a SQLite database and generate a Julia file with model definitions.

Arguments

  • db::String="db": The database key from the configuration (must resolve to a registered SQLite connection).
  • force_replace::Bool=false: Whether to overwrite the file if it already exists.
  • ignore_schema::Vector{String}=sqlite_ignore_schema: Table name patterns to ignore.
  • include_table::Union{Vector{String},Nothing}=nothing: When set, only these tables are imported.
  • file::String="automatic_models.jl": The output filename for the generated models.

Description

Symmetric with import_models_from_postgres: the database key resolves to its settings via Configuration.get_settings, the output directory comes from that connection's db_def_folder, and the connection object is taken from the same settings — so the key and its settings can never drift apart. A missing/unregistered key raises a clear ArgumentError from get_settings (no silent MODEL_PATH fallback); a key bound to a non-SQLite connection is rejected explicitly.

Example

using PormG

# Load the SQLite configuration
PormG.Configuration.load("db_sl")

# Import models from the database
PormG.Migrations.import_models_from_sqlite("db_sl")
source
PormG.Migrations.init_migrationsMethod
init_migrations(connection::Union{PormGPostgres, PormGSQLite})

Create the pormg_migrations history table if it does not already exist. This is called automatically by migrate() and status() but can be invoked explicitly for bootstrapping.

source
PormG.Migrations.makemigrationsMethod
makemigrations(db::String; interactive = true)
makemigrations(connection, settings::PormGSettings; path = "db/models.jl", interactive = true)

Compare your models.jl against the live database and write the pending migration plan. The first form is the one to call: db is a connection key from your configuration, e.g. makemigrations("db").

It does not touch the schema. The generated DDL lands in <db_def_folder>/migrations/pending_migrations.jl for review; apply it with PormG.Migrations.migrate(db).

Keyword arguments

  • path: the models file. Defaults to <db>/<settings.model_file> in the String form.
  • 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. false answers "new table" for everything and so never renames; use it in CI, not on real data.

Returns nothing. Logs and returns early — writing no plan — when the connection has change_db: false. An up-to-date schema logs that no migrations are pending. A missing models file raises MissingConfigurationError.

See also migrate, get_migration_plan, and the Database Migrations in PormG guide.

source
PormG.Migrations.mark_appliedMethod
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_failedMethod
mark_failed(connection, settings, version)

Update an existing migration record to 'failed' status. Useful after manual investigation of a partially-applied migration.

source
PormG.Migrations.migrateMethod
migrate(connection::PormGBackend, settings; interactive, destructive, dry_run_only, name)

Apply pending migrations to a database (PostgreSQL or SQLite). This is the shared pre-flight for every backend; the backend-specific execution step is dispatched to _run_locked_lifecycle (advisory lock on PostgreSQL, direct on SQLite).

Lifecycle

  1. Validate: check change_db, install configured extensions, load plan, detect destructive ops
  2. Confirm: destructive guard + interactive confirmation (TTY-aware — see _confirm_migration)
  3. Execute: run SQL in a transaction (under an advisory lock on PostgreSQL)
  4. Record: insert history into pormg_migrations
  5. Archive: move files to applied_migrations/

Keywords

  • interactive::Bool=true: prompt for confirmation before applying — only when stdin is a real terminal. In a non-interactive process (CI, Pkg.test, deploy script) no prompt is shown and migrate() never blocks on readline().
  • destructive::Bool=false: must be true to allow DROP TABLE / DROP COLUMN operations. A destructive plan in a non-interactive context throws DestructiveMigrationError unless this is set.
  • dry_run_only::Bool=false: if true, only analyze without applying (returns DryRunResult)
  • name::String="pending_migration": name for this migration in the history table
source
PormG.Migrations.migrate_toMethod
migrate_to(connection, settings, target_version; interactive, destructive)

Apply pending migrations up to (and including) a specific version. Only meaningful when multiple migration files exist in the pending queue. For now, this validates the target version against the current history.

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

source
PormG.Migrations.statusMethod
status(connection, settings) -> MigrationStatus

Report migration status: applied, failed, pending, and drift signals. Includes basic drift detection by comparing live database tables against the history of applied migrations.

source
PormG.Configuration.current_transaction_depthMethod
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.get_tx_connectionMethod
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.

source
PormG.Configuration.is_loadedMethod
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_manyMethod
load_many(paths::AbstractVector{<:AbstractString}; env::Union{Nothing,String} = nothing)

Load several static configuration folders using the same environment override. Returns the list of connection keys that were loaded.

source
PormG.Configuration.pingMethod
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.register_connectionMethod
register_connection(key::String, url::String; adapter::String = "PostgreSQL", pool_size::Int = 3)

Register a new database connection pool dynamically using a connection URL. Useful for multi-tenant applications or connecting to dynamic data sources.

source
PormG.Configuration.set_before_connect_hookMethod
set_before_connect_hook(f::Function)

Register a callback invoked before PormG opens a physical database connection. The callback receives (key::String, settings::Settings) and must return true to proceed or false to abort the connection attempt.

It runs only when a new connection must be opened (not on connection reuse) and is invoked outside the pool lock. Decide inside the callback which connections need setup, e.g. basename(settings.db_def_folder) == "db_esus".

Typical uses include VPN setup, credential refresh, or SSH tunnel activation. When no hook is registered, connections proceed normally.

source
PormG.Configuration.set_connection_resolverMethod
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.statusMethod
status(path_or_key::String) -> NamedTuple

Return a compact server-oriented status payload describing whether a configuration is loaded and reachable.

source
PormG.Configuration.with_tx_contextMethod
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 inherits the outer block's SQLite reserved-primary-key reservations rather than starting a fresh table. Being a ScopedValue it is dynamically scoped — tasks spawned inside the block inherit it, and it unwinds automatically, including on a throw.

source
PormG.ConnectionPool.FetchTaskType
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 (a LibPQ.AsyncResult for PG, a Task for SQLite)
  • pool::Union{PormGPostgres, PormGSQLite}: 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)
  • 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.PoolConnectErrorType
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.

source
PormG.ConnectionPool.PoolTimeoutErrorType
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.fetchMethod
fetch(connection::Union{PormGPostgres, PormGSQLite}, sql::String; params=nothing, ignore_tx=false) -> Tables.rowtable

Execute a database query synchronously (blocking). Internally uses async execution but immediately awaits the result.

source
PormG.ConnectionPool.acquire_connectionMethod
acquire_connection(pool::PormGPostgres; timeout_seconds=nothing, max_retries=300)
acquire_connection(pool::PormGSQLite; timeout_seconds=nothing, max_retries=300, mode=:any)

Lease a connection from the pool. You own it until you give it back — every acquire_connection must be paired with a release_connection, and the release belongs in a finally so an exception cannot leak the slot:

conn = acquire_connection(pool)
try
    # … use conn …
finally
    release_connection(pool, conn)
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.

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

See also release_connection, pool_stats, and the Advanced Configuration guide for pool tuning and leak detection.

source
PormG.ConnectionPool.atomicMethod
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
    # …outer transaction still usable here
  end
end

Pass durable=true to require this block be the outermost transaction — it throws if a transaction is already active (mirrors Django's atomic(durable=True)).

source
PormG.ConnectionPool.await_resultMethod
await_result(ft::FetchTask) -> result

Await the completion of an async fetch task and return the result. Automatically releases the connection back to the pool.

This function is idempotent - calling it multiple times returns the same result without re-releasing the connection.

source
PormG.ConnectionPool.fetch_asyncMethod
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().

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_copyMethod
fetch_copy(connection::PormGPostgres, sql::String, data_itr)

Execute a PostgreSQL COPY FROM STDIN operation using an iterable of data chunks. The driver-specific streaming (LibPQ.CopyIn + result drain) lives in the PostgreSQL extension as backend_copy_in!.

source
PormG.ConnectionPool.finalize_transaction_connection!Method
finalize_transaction_connection!(pool, conn; rollback_error=nothing) -> 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.

source
PormG.ConnectionPool.pool_statsMethod
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_connectionMethod
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_transactionMethod
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_savepointMethod
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_transactionMethod
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.
  • release_conn: return the connection to the pool when this call finishes. 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.

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_asyncMethod
with_transaction_async(pool::PormGPostgres, sql::String; ...) -> (task, conn)

Start an async transaction query. Returns the async handle and connection. The connection is NOT released - caller must manage it for transaction continuation.

For transactions, you typically want to keep the connection for multiple queries.

source
PormG.ConnectionPool.without_foreign_keysMethod
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 both backends, so a block that is transiently inconsistent — writing children before their parents — commits without any special handling. Reach for this only when that is not enough: a load too large for one transaction, a repair that must leave a violation in place, or a test that plants one deliberately.

db may be a pool, a PormGSettings, or a db-key String, like atomic. Every query inside f() reuses the pinned connection, and a nested atomic becomes a SAVEPOINT.

This block must be the outermost transaction on its pool. It cannot nest inside run_in_transaction/atomic and raises TransactionError if you try: suspension works by setting PRAGMA foreign_keys = OFF, which SQLite silently ignores while a transaction is open, so a nested call could not suspend anything.

With check_on_exit = true (the default) a PRAGMA foreign_key_check runs before COMMIT and rolls the block back if it finds an orphaned row, raising UnsafeMutationError — so the escape hatch cannot quietly commit a corrupt database.

Note the check is whole-database, not scoped to what f() touched: on a database that already contains orphans, it will abort a block that did nothing wrong. That is deliberate (it is the same PRAGMA foreign_key_check the migration rebuild gate uses, and narrowing it would mean guessing which rows the block touched), but it means a repair run against an already-inconsistent database wants check_on_exit = false — as does deliberately planting a violation.

The connection is renewed, not returned as-is, so a suspended handle can never serve another caller (#276).

Example

# A load too large to hold in one transaction: commit each chunk, tolerating the inconsistency
# between them, and let the exit check prove the finished result is sound.
without_foreign_keys(pool) do
    for chunk in Iterators.partition(eachrow(results_df), 50_000)
        bulk_insert(M.Result, DataFrame(chunk))
    end
    bulk_insert(M.Race, races_df)
end

# For a merely transient inconsistency, a plain transaction is enough — and cheaper, since it does
# not have to renew the connection afterwards:
atomic(pool) do
    bulk_insert(M.Result, results_df)   # children
    bulk_insert(M.Race,   races_df)     # parents — checked at COMMIT, on both backends
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 a violation still surfaces, just later. check_on_exit is SQLite-only.

See also atomic, run_in_transaction.

source
PormG.Utils.@import_modelsMacro
@import_models(path, alias)

Includes a model file and automatically registers the defined module with PormG.

The file at path must define its own module (e.g., module my_models ... end). The alias must match the module name defined in the file.

The macro automatically:

  • Resolves the path relative to the calling file
  • Registers models for the current session
  • Injects an __init__() function for post-precompilation re-registration
  • Handles World Age issues in Julia 1.12+

Users do NOT need to manually add an __init__() function to their model files.

Example

# In your package's main module:
PormG.@import_models "db/models.jl" models
import .models as M

# Now use M.Driver, M.Result, etc.
# Models are automatically available here and after precompilation
source
PormG.Utils.@models_moduleMacro
@models_module(name, path, block)

Defines a new module with inline model definitions and registers it with PormG.

Use this when you want to define models directly in code rather than in a separate file.

Example

@models_module MyDB "db" begin
    Driver = Models.Model("drivers",
        id = Models.IDField(),
        name = Models.CharField()
    )
end
source