PormG.jl is a Django-inspired ORM for Julia with async-first execution, designed for high-concurrency web frameworks like Genie.jl. It provides an expressive query builder, automatic migrations, and cross-database support for PostgreSQL and SQLite.
Full documentation, tutorials, and API reference: https://pingolee.github.io/PormG.jl/dev/
- Async-first β non-blocking I/O via
LibPQ.async_execute; synchronous helpers are thin wrappers that never block the event loop - Django-style filter syntax β
__for join traversal,__@gt/__@in/__@containsfor operators - F-expressions β column arithmetic and field-to-field comparisons:
F("points") > F("grid") - Aggregation & annotation β
Count,Sum,Avg,Max,MinwithGROUP BY/HAVINGhandled automatically - Migrations β state-based schema reconciliation with
makemigrations/migrate, destructive-operation guards, and a history table - Multi-database & multi-tenancy β switch connections at runtime with
.db("tenant_id") - Advisory locks β distributed coordination via
with_advisory_lock - Transactions β
run_in_transactionwith savepoint support and async context propagation
- Julia 1.12 or newer β the 1.12 floor is deliberate: the hot-reload model-loading machinery (
@import_models/set_models) relies on Julia 1.12 world-age semantics, so the 1.10 LTS is not targeted (#211) - A SQL driver for your backend β
LibPQ(PostgreSQL) orSQLite
PormG is not yet registered in the Julia General Registry. Install the development version:
using Pkg
Pkg.add(url="https://github.com/PingoLee/PormG.jl")Once registered, this becomes:
using Pkg
Pkg.add("PormG")PormG does not pull in a SQL driver automatically β LibPQ (PostgreSQL) and SQLite are
weak dependencies. Install and load the one your app uses as a direct dependency:
Pkg.add("LibPQ") # PostgreSQL
Pkg.add("SQLite") # SQLiteA bare using PormG loads the ORM but no backend; the first query then raises a clear error
telling you to using LibPQ (or using SQLite).
using PormG
PormG.setup() # interactive: writes db/connection.yml and a db/models.jl skeletonPrefer to wire it up by hand? The next steps show the files setup() would create.
db/connection.yml:
default_env: dev
dev:
adapter: PostgreSQL # or SQLite
database: your_database
host: 'localhost'
username: your_username
password: your_password
port: 5432
config:
change_db: true # allow schema migrations
change_data: true # allow data mutations
time_zone: 'America/Sao_Paulo'Note: If
config:is omitted, PormG applies safety-first defaults βchange_dbandchange_dataarefalse(migrations and writes disabled). Set them totrueexplicitly to enable schema changes and data mutations.
# db/models.jl
module models
import PormG.Models
Driver = Models.Model("drivers",
driverid = Models.IDField(),
forename = Models.CharField(max_length=50),
surname = Models.CharField(max_length=50),
nationality = Models.CharField(max_length=50),
dob = Models.DateField(null=true),
)
Constructor = Models.Model("constructors",
constructorid = Models.IDField(),
name = Models.CharField(max_length=50),
nationality = Models.CharField(max_length=50),
)
Race = Models.Model("races",
raceid = Models.IDField(),
name = Models.CharField(max_length=255),
year = Models.IntegerField(),
date = Models.DateField(),
)
Result = Models.Model("results",
resultid = Models.IDField(),
raceid = Models.ForeignKey(Race, pk_field="raceid", on_delete="CASCADE"),
driverid = Models.ForeignKey(Driver, pk_field="driverid", on_delete="RESTRICT"),
constructorid = Models.ForeignKey(Constructor, pk_field="constructorid", on_delete="RESTRICT"),
positionorder = Models.IntegerField(),
points = Models.FloatField(),
)
endNote: PormG preserves the case you declare field names with, and field lookups are case-sensitive. The recommended house style is lowercase
snake_case; reserve mixed-case declarations for mapping existing columns you don't control.
using PormG, LibPQ, DataFrames # LibPQ β PostgreSQL; swap in SQLite for the SQLite backend
using PormG.Functions: Count # SQL functions (Count, Sum, Max, β¦) β namespaced
PormG.Configuration.load("db") # loads db/connection.yml (must precede @import_models)
PormG.@import_models "db/models.jl" models
import .models as MPormG.Migrations.makemigrations("db") # analyze models, generate a migration
PormG.Migrations.migrate("db") # apply it to the database# All Brazilian race winners β INNER JOINs resolved automatically from the __ paths
df = M.Result.objects.filter(
"driverid__nationality" => "Brazilian",
"positionorder" => 1,
).values(
"driverid__forename",
"driverid__surname",
"raceid__year",
"raceid__name",
).order_by("-raceid__year") |> DataFrame# Wins per constructor, using aggregation (GROUP BY handled automatically)
df = M.Result.objects.filter(
"positionorder" => 1
).values(
"constructorid__name",
"wins" => Count("resultid"),
).order_by("-wins") |> DataFrameJulia method-chain gotcha: multi-line chains must use trailing-dot syntax (the
.at the end of the line) or stay inline. A leading dot on the next line is a JuliaParseError.# β trailing dot df = M.Driver.objects. filter("nationality" => "Brazilian"). list() # β leading dot β ParseError df = M.Driver.objects .filter("nationality" => "Brazilian") .list()
# Single insert
M.Driver.objects.create(
"forename" => "Ayrton",
"surname" => "Senna",
"nationality" => "Brazilian",
)
# Bulk insert from a DataFrame β call on the .objects handler to respect the ORM boundary
df = DataFrame([
Dict("forename" => "Alain", "surname" => "Prost", "nationality" => "French"),
Dict("forename" => "Nelson", "surname" => "Piquet", "nationality" => "Brazilian"),
])
bulk_insert(M.Driver.objects, df)
# Update matching rows in place
M.Driver.objects.filter("nationality" => "Brazilian").update("nationality" => "Brazil")
# Atomic F-expression update (no read-modify-write race)
M.Result.objects.filter("resultid" => 1).update("points" => F("points") + 10)PormG is wire-format compatible with tables managed by Django, with identical column
serialization and mutation semantics across PostgreSQL and SQLite β TIMESTAMPTZ handling,
DateField truncation, DecimalField precision (NUMERIC-backed), and auto_now /
auto_now_add temporal fields. See
Import from Django for details.
Contributions are welcome β please open an issue or pull request on GitHub. See the Contributing & Debugging page for the development workflow, debugging guide, and testing conventions.
MIT License β see LICENSE.