Skip to content

Latest commit

Β 

History

692 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

PormG.jl

Docs (dev) CI Julia License: MIT

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.

πŸ“š Documentation

Full documentation, tutorials, and API reference: https://pingolee.github.io/PormG.jl/dev/

Key Features

  • 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 / __@contains for operators
  • F-expressions β€” column arithmetic and field-to-field comparisons: F("points") > F("grid")
  • Aggregation & annotation β€” Count, Sum, Avg, Max, Min with GROUP BY / HAVING handled 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_transaction with savepoint support and async context propagation

Requirements

  • 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) or SQLite

Installation

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

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


Quick Start

1. Scaffold a project (optional)

using PormG
PormG.setup()   # interactive: writes db/connection.yml and a db/models.jl skeleton

Prefer to wire it up by hand? The next steps show the files setup() would create.

2. Configure the connection

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_db and change_data are false (migrations and writes disabled). Set them to true explicitly to enable schema changes and data mutations.

3. Define models

# 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(),
)
end

Note: 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.

4. Load configuration and models

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 M

5. Run migrations

PormG.Migrations.makemigrations("db")   # analyze models, generate a migration
PormG.Migrations.migrate("db")          # apply it to the database

6. Query 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") |> DataFrame

Julia 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 Julia ParseError.

# βœ“ trailing dot
df = M.Driver.objects.
    filter("nationality" => "Brazilian").
    list()

# βœ— leading dot β†’ ParseError
df = M.Driver.objects
    .filter("nationality" => "Brazilian")
    .list()

7. Create and update records

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

Django Compatibility

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.


Contributing

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.

License

MIT License β€” see LICENSE.

About

A Julia ORM inspired by the Django ORM.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages