Skip to main content

A production-grade async MySQL ORM with LoopBack-style nested relation queries, typed model instances, and zero N+1.

Project description

oceanic-mysql-orm

A production-grade async MySQL connector with LoopBack-style nested relation queries and fully typed model instances. Built for FastAPI.


Features

  • Declarative models — define once, auto-registered
  • LoopBack-style filter DSL (where, order, limit, skip, fields, include)
  • Nested relation loading with zero N+1 queries (batch IN-clause strategy)
  • Typed model instances returned — full autocomplete, no raw dicts
  • RelationNotLoadedError if you access an unloaded relation — no silent bugs
  • All 4 relation types: hasOne, hasMany, belongsTo, hasManyThrough
  • Scoped includes — filter/sort/limit inside each relation independently
  • Async connection pool (aiomysql)
  • Transaction support
  • Raw SQL escape hatch

Install

pip install oceanic-mysql-orm

Or from source:

git clone https://gitlab.com/MaheshPulivarthi181/mysql-orm-connector.git
cd mysql-orm-connector
pip install .

For a detailed guide, see USAGE.md.


Quick Start

1. Define your models

# models.py
from mysql_connector import MySQLModel, Field, Relation

class User(MySQLModel):
    __table__ = "users"

    id:     int           = Field(primary_key=True)
    name:   str           = Field(max_length=100)
    email:  str           = Field(unique=True)
    active: bool          = Field(default=True)

    orders:  list["Order"]  = Relation(type="hasMany", foreign_key="user_id")
    profile: "Profile"      = Relation(type="hasOne",  foreign_key="user_id")

class Order(MySQLModel):
    __table__ = "orders"

    id:      int    = Field(primary_key=True)
    user_id: int    = Field(foreign_key="users.id")
    total:   float
    status:  str    = Field(default="pending")

    user:  "User"           = Relation(type="belongsTo", foreign_key="user_id")
    items: list["OrderItem"] = Relation(type="hasMany",  foreign_key="order_id")

class OrderItem(MySQLModel):
    __table__ = "order_items"

    id:         int   = Field(primary_key=True)
    order_id:   int   = Field(foreign_key="orders.id")
    product_id: int   = Field(foreign_key="products.id")
    qty:        int   = Field(default=1)
    unit_price: float

    product: "Product" = Relation(type="belongsTo", foreign_key="product_id")

2. Setup the connector

from mysql_connector import MySQLConnector
import models  # importing triggers auto-registration

connector = MySQLConnector(
    host="localhost", database="mydb",
    user="root", password="secret",
)
await connector.connect()

3. Query with nested relations

# Simple include
users = await connector.find(User, {
    "include": ["orders"],
})

# Deep nested — 3 levels, only 4 SQL queries total
users = await connector.find(User, {
    "where":   {"active": True},
    "include": ["orders.items.product"],
    "limit":   10,
})
for user in users:
    print(user.name)
    for order in user.orders:
        for item in order.items:
            print(f"  {item.product.name} x{item.qty}")

# Scoped include — filter inside the relation
users = await connector.find(User, {
    "include": [
        {
            "relation": "orders",
            "scope": {
                "where":   {"status": "paid"},
                "order":   ["created_at DESC"],
                "limit":   5,
                "include": [
                    {
                        "relation": "items",
                        "scope": {
                            "where":   {"qty": {"gt": 0}},
                            "include": ["product"],
                        }
                    }
                ]
            }
        }
    ]
})

Filter DSL Reference

filter = {
    # WHERE conditions
    "where": {
        "status":  "active",                    # equality
        "age":     {"gt": 18},                  # >
        "price":   {"gte": 100, "lte": 500},    # BETWEEN-style
        "name":    {"like": "%raj%"},            # LIKE
        "role":    {"inq": ["admin", "user"]},  # IN
        "deleted": {"neq": True},               # !=
        "or": [                                 # OR
            {"status": "paid"},
            {"status": "refunded"},
        ],
    },
    "order":   ["name ASC", "created_at DESC"], # ORDER BY
    "limit":   20,                              # LIMIT
    "skip":    40,                              # OFFSET
    "fields":  ["id", "name", "email"],         # SELECT specific columns
    "include": ["orders.items.product"],        # nested relations
}

Supported operators

Operator SQL equivalent
eq = %s
neq != %s
gt > %s
gte >= %s
lt < %s
lte <= %s
like LIKE %s
nlike NOT LIKE %s
inq IN (...)
nin NOT IN (...)
between BETWEEN %s AND %s
regexp REGEXP %s

CRUD Reference

# Find many
users = await connector.find(User, filter)

# Find one
user = await connector.find_one(User, {"where": {"email": "..."}})

# Find by PK
user = await connector.find_by_id(User, 42, {"include": ["orders"]})

# Create
user = await connector.create(User, {"name": "Raj", "email": "raj@example.com"})

# Update by PK — returns updated instance
user = await connector.update_by_id(User, 42, {"name": "Rajesh"})

# Update many
count = await connector.update_all(User, where={"active": False}, data={"deleted": True})

# Delete by PK
deleted = await connector.delete_by_id(User, 42)  # bool

# Delete many
count = await connector.delete_all(User, {"active": False})

# Count
n = await connector.count(User, {"active": True})

# Exists
exists = await connector.exists(User, {"email": "raj@example.com"})

# Raw SQL
rows = await connector.raw("SELECT * FROM users WHERE id = %s", [1])

# Transaction
async with connector.transaction() as conn:
    async with conn.cursor(aiomysql.DictCursor) as cur:
        await cur.execute("INSERT INTO users ...")
        await cur.execute("INSERT INTO orders ...")

FastAPI integration

from fastapi import FastAPI
from mysql_connector import MySQLConnector
import models   # auto-registers all models

connector = MySQLConnector(host="localhost", database="mydb", ...)

@asynccontextmanager
async def lifespan(app):
    await connector.connect()
    yield
    await connector.disconnect()

app = FastAPI(lifespan=lifespan)

@app.get("/users/{id}")
async def get_user(id: int):
    user = await connector.find_by_id(User, id, {
        "include": ["orders.items.product", "profile"]
    })
    if not user:
        raise HTTPException(404)
    return user.to_dict()   # fully serialized with nested objects

Project Structure

src/mysql_connector/
├── __init__.py           # public API exports
├── connector.py          # MySQLConnector — main entry point
├── model.py              # MySQLModel base class + ModelMeta metaclass
├── fields.py             # Field() and Relation() descriptors
├── registry.py           # global model registry (singleton)
├── pool.py               # async connection pool (aiomysql)
├── query_builder.py      # filter DSL → parameterized SQL
├── relation_resolver.py  # batch relation loading (no N+1)
├── types.py              # FilterOptions, FieldDefinition, etc.
└── exceptions.py         # typed exceptions

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

oceanic_mysql_orm-1.0.4.tar.gz (30.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oceanic_mysql_orm-1.0.4-py3-none-any.whl (30.0 kB view details)

Uploaded Python 3

File details

Details for the file oceanic_mysql_orm-1.0.4.tar.gz.

File metadata

  • Download URL: oceanic_mysql_orm-1.0.4.tar.gz
  • Upload date:
  • Size: 30.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for oceanic_mysql_orm-1.0.4.tar.gz
Algorithm Hash digest
SHA256 6ea144754fbd8ab0575093a9a80614f12a7cab3f7f70eef11ce6c6970deb5669
MD5 2f257fbdcd5553e56594785a7bc8c12f
BLAKE2b-256 428eb3d72e946271a409793960acb3205d01dc3e3be23d70b75bfc03d516bac4

See more details on using hashes here.

File details

Details for the file oceanic_mysql_orm-1.0.4-py3-none-any.whl.

File metadata

File hashes

Hashes for oceanic_mysql_orm-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 b28b88368561d66b5b76f5f8f72edf5bdf857dd311124a29cc2e38eabc7ef939
MD5 4a9f1f891e80ccf5ff5c8af48f14d695
BLAKE2b-256 7362fbb054fa2df2f5ecbec6a13a1cabe6bff34a711e421e93bc1f52d6d39ce0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page