Skip to main content

A minimal, plugin-driven ORM for understanding and debugging SQL

Project description

DebugORM

A minimal, plugin-driven ORM for SQLite whose primary goal is helping developers understand and debug SQL queries, not just execute them.


Architecture

Model → QuerySet → Query → Pipeline → [Plugins] → SQLCompiler → Executor → Result

Key objects

Object Responsibility
Field Declares a column; validates and converts values
Model Base class; provides save(), delete(), create_table()
Manager Attached as Model.objects; creates QuerySets
QuerySet Chainable, lazy query builder
Query Structured, SQL-free representation of a query (the internal AST)
SQLCompiler Translates Query(sql, params)
QueryPipeline Orchestrates the full lifecycle and calls plugin hooks
Plugin Base class for all debug extensions

Plugin hook order

before_compile(query, ctx)
    ↓
SQLCompiler.compile(query) → CompiledQuery
    ↓
after_compile(compiled, ctx)
    ↓
before_execute(compiled, ctx)   ← return QueryResult here to skip execution
    ↓
QueryExecutor.execute(compiled) → QueryResult
    ↓
after_execute(result, ctx)

Quick start

from debugorm import configure, Model
from debugorm.fields import IntegerField, StringField

configure(":memory:")       # or a file path: "myapp.db"

class User(Model):
    id   = IntegerField(primary_key=True)
    name = StringField(max_length=100)
    age  = IntegerField()

User.create_table()

CRUD

# Create
alice = User(name="Alice", age=25)
alice.save()                        # INSERT

alice.age = 26
alice.save()                        # UPDATE

alice.delete()                      # DELETE

# Read
User.objects.all()
User.objects.filter(age__gte=18).order_by("-age").all()
User.objects.get(name="Bob")
User.objects.filter(age__gt=20).count()
User.objects.order_by("-age").first()

# OR conditions
User.objects.filter(name="Alice").or_filter(name="Bob").all()
# WHERE (name = 'Alice') OR (name = 'Bob')

# Multiple conditions per OR group
User.objects.filter(age__gt=28).or_filter(age__lt=18, name="Bob").all()
# WHERE (age > 28) OR (age < 18 AND name = 'Bob')

# Exclude
User.objects.exclude(age__lt=18).all()
# WHERE NOT (age < 18)

User.objects.exclude(age__lt=18).exclude(name="Admin").all()
# WHERE NOT (age < 18) AND NOT (name = 'Admin')

# Aggregations
from debugorm import Avg, Max, Min, Sum, Count

User.objects.aggregate(avg_age=Avg("age"), max_age=Max("age"), total=Count())
# {"avg_age": 25.0, "max_age": 30, "total": 4}

User.objects.filter(age__gte=18).aggregate(avg_age=Avg("age"))
# {"avg_age": 25.7}

# values() — return dicts
User.objects.filter(age__gt=18).values("id", "name").all()
# [{"id": 1, "name": "Alice"}, ...]

# values_list() — return tuples
User.objects.values_list("id", "name").all()
# [(1, "Alice"), (2, "Bob"), ...]

User.objects.values_list("name", flat=True).all()
# ["Alice", "Bob", ...]

Supported lookup operators

Suffix SQL
age__eq=18 or age=18 age = 18
age__ne=18 age != 18
age__gt=18 age > 18
age__gte=18 age >= 18
age__lt=18 age < 18
age__lte=18 age <= 18
name__like="A%" name LIKE 'A%'
name__in=["Alice","Bob"] name IN ('Alice', 'Bob')
name__isnull=True name IS NULL

Debug plugins

Attach plugins on the fly with .debug():

User.objects.debug(
    explain=True,       # EXPLAIN QUERY PLAN
    dry_run=True,       # show SQL without executing
    log=True,           # log SQL + timing via Python logging
    visualize=True,     # print query tree
    pretty=True,        # pretty-format SQL
).filter(age__gt=18).all()

ExplainPlugin

Runs EXPLAIN QUERY PLAN after query execution and prints how SQLite plans to process it.

╔────────────────────────────────────────────────────────────╗
║  [ExplainPlugin]
╠────────────────────────────────────────────────────────────╣
║  SQL  : SELECT * FROM users WHERE age > ? ORDER BY age DESC
╠────────────────────────────────────────────────────────────╣
║  QUERY PLAN
║    └── SCAN users
║    └── USE TEMP B-TREE FOR ORDER BY
╚────────────────────────────────────────────────────────────╝

DryRunPlugin

Prevents execution; estimates how many rows would be affected.

╔────────────────────────────────────────────────────────────╗
║  [DryRunPlugin]  ⚠  NOT EXECUTED
╠────────────────────────────────────────────────────────────╣
║  SQL   : SELECT * FROM users WHERE age >= ? ORDER BY name ASC
║  Interpolated: SELECT * FROM users WHERE age >= 18 ORDER BY name ASC
╠────────────────────────────────────────────────────────────╣
║  Estimated rows affected: ~3
╚────────────────────────────────────────────────────────────╝

LoggingPlugin

Uses Python's logging module — integrates with any existing log config.

DEBUG  debugorm.sql › SQL ▶ SELECT * FROM users WHERE age > ?  params=(20,)
DEBUG  debugorm.sql › SQL ◀ rows=3  time=0.12ms

VisualizePlugin

Renders the query as a tree before compilation (shows intent, not SQL).

╔────────────────────────────────────────────────────────────╗
║  [VisualizePlugin]  Query Structure
╠────────────────────────────────────────────────────────────╣
║  User (SELECT)
║   ├── filter: age > 18
║   ├── filter: name LIKE 'A%'
║   ├── order_by: name ASC
║   └── limit: 10
╚────────────────────────────────────────────────────────────╝

Pass show_json=True to also get the JSON representation.

PrettyPrintPlugin

Formats the compiled SQL for human readability.

SELECT *
  FROM users
  WHERE age >= ?
  ORDER BY age DESC
  LIMIT ?

Query Diff

Compare two queries at the internal representation level (not string comparison):

q1 = User.objects.filter(age__gt=18)
q2 = User.objects.filter(age__gt=21)

print(q1.diff(q2))
# - age > 18
# + age > 21

q3 = User.objects.filter(age__gte=18).order_by("-age").limit(10)
q4 = User.objects.filter(age__gte=18).order_by("name").limit(5)

print(q3.diff(q4))
# - ORDER BY age DESC
# + ORDER BY name ASC
# - LIMIT 10
# + LIMIT 5

print(q1.diff(q1))
# Queries are identical

Global plugin configuration via DebugORM facade

from debugorm import DebugORM, LoggingPlugin, PrettyPrintPlugin

orm = DebugORM(":memory:", plugins=[LoggingPlugin(), PrettyPrintPlugin()])
orm.install()   # applies the pipeline to every already-defined model

Project structure

debugorm/
├── __init__.py          public API
├── core/
│   ├── model.py         Model base class + ModelMeta
│   ├── query.py         Query AST + FilterCondition + Query.diff()
│   ├── compiler.py      SQLCompiler → CompiledQuery
│   ├── pipeline.py      QueryPipeline + QueryExecutor + QueryResult
│   └── manager.py       Manager + QuerySet
├── fields/
│   ├── base.py
│   ├── integer.py
│   └── string.py
├── plugins/
│   ├── base.py          Plugin base class + hook signatures
│   ├── explain.py       ExplainPlugin
│   ├── dry_run.py       DryRunPlugin
│   ├── logging_plugin.py LoggingPlugin
│   ├── visualize.py     VisualizePlugin
│   └── pretty_print.py  PrettyPrintPlugin
└── db/
    └── connection.py    Connection + configure()
demo.py                  end-to-end usage example

Writing a custom plugin

from debugorm.plugins.base import Plugin

class TimingPlugin(Plugin):
    name = "timing"

    def before_execute(self, compiled, ctx):
        import time
        ctx.extra["t0"] = time.perf_counter()
        return None   # don't intercept

    def after_execute(self, result, ctx):
        elapsed = (time.perf_counter() - ctx.extra["t0"]) * 1000
        print(f"[TimingPlugin] {elapsed:.1f}ms — {result.rowcount} rows")


# Use it:
User.objects.debug().filter(age__gt=18)   # ... or
from debugorm import QueryPipeline
User.objects.pipeline = QueryPipeline(plugins=[TimingPlugin()])

Requirements

  • Python 3.10+
  • No external dependencies (standard library only: sqlite3, logging, dataclasses, copy, re, time, json)

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

debugorm-0.1.1.tar.gz (27.3 kB view details)

Uploaded Source

Built Distribution

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

debugorm-0.1.1-py3-none-any.whl (27.1 kB view details)

Uploaded Python 3

File details

Details for the file debugorm-0.1.1.tar.gz.

File metadata

  • Download URL: debugorm-0.1.1.tar.gz
  • Upload date:
  • Size: 27.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for debugorm-0.1.1.tar.gz
Algorithm Hash digest
SHA256 6ca1d4534b730c1ada7fccfb9a9d91e5e5e21eae5e553c4e22f79b3fa95ae491
MD5 7bd6b017b168ed81b780d7b35bde7190
BLAKE2b-256 8e0218d2db7b5c83eb98ac50b0dd2ebbf1f26f7dd132c84d5df82430e9bbbb33

See more details on using hashes here.

Provenance

The following attestation bundles were made for debugorm-0.1.1.tar.gz:

Publisher: publish.yml on dgolov/debugorm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file debugorm-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: debugorm-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 27.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for debugorm-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 cdff00f20291398e340cab83f9dc330d85226865b4c065da58355ab8b81f5911
MD5 1b5dda834bcc19f66ca647de35067cee
BLAKE2b-256 23c32a86c6cf40839252eaff60128eb643d01a75201dd45f58473599b72dd501

See more details on using hashes here.

Provenance

The following attestation bundles were made for debugorm-0.1.1-py3-none-any.whl:

Publisher: publish.yml on dgolov/debugorm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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