PormG.jl Documentation

PormG.jl is a Django-inspired ORM for Julia, built with an async-first architecture for high-concurrency web frameworks. It brings the expressive power of Django's query builder to Julia while leveraging Julia's performance and type system.


Why PormG?

FeatureDescription
Django-Style Query BuilderFamiliar syntax using filter, values, order_by, and chainable methods. Join traversal with __ notation.
Async-First ExecutionNon-blocking I/O via LibPQ.async_execute. Synchronous helpers are thin wrappers — the event loop is never blocked.
Cross-Database SupportPostgreSQL (LibPQ.jl) and SQLite (SQLite.jl) with automatic connection pooling and dialect adaptation.
Multi-Database & Multi-TenancySwitch databases at runtime with .db("tenant_id"). Lazy connection resolution via resolver functions.
F-Expressions & AggregationsColumn arithmetic, field-to-field comparisons, Count, Sum, Avg, Max, Min with automatic GROUP BY / HAVING.
MigrationsState-based schema reconciliation with makemigrations / migrate, destructive-operation guards, and a history table.
Transactionsrun_in_transaction with async context propagation, savepoint support, and automatic rollback on error.
Advisory LocksDistributed coordination via with_advisory_lock for safe concurrent processes.
Password HandlingDjango-compatible PBKDF2-SHA256 hashing, BCrypt, Argon2, Spring Security interop, and password validation.
Terminal DashboardOptional Tachikoma-based TUI for reviewing migrations and inspecting queries.

Installation

PormG is currently in early development and is not yet registered in the Julia General Registry. Install the development version using the Julia package manager:

using Pkg
Pkg.add(url="https://github.com/PingoLee/PormG.jl")

Or develop locally:

using Pkg
Pkg.develop(url="https://github.com/PingoLee/PormG.jl")

Note: Since this is a development package, features may change and stability is not guaranteed. Please report any issues on the GitHub repository.


Quick Start

1. Initialize Your Project

using PormG
PormG.setup() # Interactive setup for database and models

This creates a db/ folder with a connection.yml template and a models.jl skeleton.

2. Configure the Database Connection

Open db/connection.yml and configure your database:

env: dev

dev:
  adapter: PostgreSQL
  database: your_database_name
  host: 'localhost'
  username: your_username
  password: your_password
  port: 5432
  config:
    change_db: true      # create the database if it doesn't exist
    change_data: true    # allow data modifications
    time_zone: 'America/Sao_Paulo'

For SQLite, use:

env: dev

dev:
  adapter: SQLite
  database: 'my_app.db'

3. Define Your Models

Create db/models.jl with your model definitions:

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

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"),
    positionOrder  = Models.IntegerField(),
    points         = Models.FloatField(),
)

Models.set_models(@__MODULE__, @__DIR__)  # Required at end of file
end

4. Load Configuration and Import Models

using PormG, DataFrames

# Load configuration — must happen BEFORE importing models
PormG.Configuration.load("db")

# Import models with hot-reload support (Revise.jl)
PormG.@import_models "db/models.jl" models
import .models as M

5. Create and Apply Migrations

PormG.Migrations.makemigrations("db")   # Analyze models and generate migration
PormG.Migrations.migrate("db")          # Apply migration to database

6. Create Records

# Single record creation
M.Driver.objects.create(
    "forename" => "Ayrton",
    "surname"  => "Senna",
    "nationality" => "Brazilian"
)

# Bulk insert for multiple records
bulk_data = DataFrame([
    Dict("forename" => "Alain",  "surname" => "Prost",    "nationality" => "French"),
    Dict("forename" => "Nelson", "surname" => "Piquet",   "nationality" => "Brazilian"),
])
bulk_insert(M.Driver, bulk_data)

7. Query Your Data

# Simple filter and list
drivers = M.Driver.objects.filter("nationality" => "Brazilian").order_by("surname").list()

# Chainable methods with DataFrame output
df = M.Result.objects.filter(
        "driverId__nationality" => "Brazilian",
        "positionOrder"         => 1
    ).values(
        "driverId__forename",
        "driverId__surname",
        "raceId__year",
    ).order_by("-raceId__year") |> DataFrame

# Aggregations
df = M.Result.objects.filter(
        "positionOrder" => 1
    ).values(
        "constructorId__name",
        "wins" => Count("resultId")
    ).order_by("-wins") |> DataFrame

8. Update and Delete

# Update matching records
M.Driver.objects.filter("nationality" => "Brazilian").update("nationality" => "Brazil")

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

# Delete matching records
M.Driver.objects.filter("surname" => "TestDriver").delete()

Architecture Overview

┌─────────────────────────────────────────────────────┐
│                    Your Application                  │
│  M.Driver.objects.filter(...).values(...).list()     │
├─────────────────────────────────────────────────────┤
│                  Query Builder (Functor API)         │
│  filter · values · order_by · limit · cjoin · on    │
├─────────────────────────────────────────────────────┤
│                 Dialect Layer                         │
│          PostgreSQL ←→ SQL Generation ←→ SQLite      │
├─────────────────────────────────────────────────────┤
│              Connection Pool (Async-First)            │
│    LibPQ.async_execute  ·  SQLite.execute            │
├─────────────────────────────────────────────────────┤
│               Configuration & Multi-Tenancy          │
│  load · load_many · register_connection · resolver   │
└─────────────────────────────────────────────────────┘

Documentation Guide

This documentation is organized into the following sections:

SectionDescription
ConfigurationDatabase connections, environments, multi-tenancy, and health checks.
ModelsDefining models, @import_models, hot-reloading, and naming conventions.
FieldsComprehensive field type reference: text, numeric, date, boolean, relationships.
Migrationsmakemigrations, migrate, dry_run, history table, destructive guards.
Writing
  OverviewAsync-first write philosophy and performance comparison.
  Creating RecordsSingle-record create() patterns.
  Updating Recordsupdate() with filters and F-expressions.
  Deleting RecordsSafe record deletion with cascading.
  Bulk Operationsbulk_insert, bulk_copy, and bulk_update.
  Transactionsrun_in_transaction, async context propagation, savepoints.
Reading
  OverviewQuery execution, output formats, and query styles.
  Values and JoinsColumn selection, __ join traversal, aliases.
  Filters and AggregatesLookup operators, grouping, and HAVING.
  Functions and DatesSQL functions and date-oriented querying.
  Subqueries and CTEsIN subqueries and With(...) CTEs.
  Field ExpressionsF() expressions, column arithmetic, aggregate ratios.
  Q ObjectsComplex boolean logic with Q, Qor, and NOT.
Import from DjangoMigrating models and data from Django projects.
Custom Joinscjoin() for runtime join conditions and on() for ON-clause predicates.
PasswordsHashing, verification, upgrading, validation, and cross-framework compatibility.
Advisory LocksDistributed locking with with_advisory_lock.
ContributingDevelopment workflow, @pormg_debug breakpoints, and testing conventions.
API ReferenceFull auto-generated API reference and exported function catalog.

Database Example: Formula 1

The examples throughout this documentation use the Formula 1 World Championship dataset (Kaggle). This provides a real-world schema with multiple related tables (Driver, Race, Circuit, Constructor, Result, Status), making it ideal for demonstrating joins, aggregations, and complex queries.

# Example: Find all Brazilian race winners with circuit information
df = M.Result.objects.filter(
        "driverId__nationality" => "Brazilian",
        "positionOrder"         => 1,
    ).values(
        "driverId__forename",
        "driverId__surname",
        "raceId__name",
        "raceId__circuitId__name",
        "raceId__year",
    ).order_by("-raceId__year") |> DataFrame

Contributing

Contributions to PormG are welcome! Please see the Contributing & Debugging page for the development workflow, testing conventions, and how to use @pormg_debug breakpoints.

License

PormG is licensed under the MIT License. See the LICENSE file for details.