Skip to main content

A lightweight, JSON-first micro-ORM for SQLite — Pydantic models as JSON documents, fluent queries, sync + async

Project description

SQLer

English | 日本語はこちら

PyPI version Python Tests License

A lightweight, JSON-first micro-ORM for SQLite. Define Pydantic models, persist them as JSON documents, query with a fluent API. Sync and async.

Install

pip install sqler

Python 3.12+ · SQLite with JSON1 (bundled on most platforms).

Quick Start

from sqler import SQLerDB, SQLerModel
from sqler.query import SQLerField as F

class User(SQLerModel):
    name: str
    age: int
    tags: list[str] = []

db = SQLerDB.in_memory()
User.set_db(db)

User(name="Alice", age=30, tags=["admin"]).save()
User(name="Bob", age=25, tags=["user"]).save()

admins = User.query().filter(F("tags").contains("admin")).all()
assert admins[0].name == "Alice"

young = User.query().filter(F("age") < 28).order_by("name").all()
assert young[0].name == "Bob"

See It in Action

sqler demo

git clone https://github.com/gabu-quest/sqler.git && cd sqler
uv run python demo.py

Interactive Tour

Learn sqler hands-on with marimo notebooks — run and modify code directly in your browser.

Launch Interactive Tour →

Run in browser (Lite models — no install needed):

Tour Topics
01. Fundamentals Models, CRUD, queries, aggregations
02. Relationships References, hydration, cross-model queries
03. Safe Models Optimistic locking, conflict resolution
04. Transactions Atomic operations, rollback
05. Mixins Timestamps, soft delete, lifecycle hooks
06. Advanced Bulk ops, indexes, integrity, raw SQL
07. Export/Import CSV, JSON, JSONL
08. Full-Text Search FTS5, boolean queries, ranking
09. Change Tracking Dirty checking, change detection
10. Database Ops Health checks, stats, vacuum, logging
11. Metrics & Caching Prometheus metrics, query caching, pools

Run locally (full Pydantic features — all 11 tours):

git clone https://github.com/gabu-quest/sqler.git && cd sqler
uv sync --dev
uv run marimo edit examples/tour_01_fundamentals.py
All 11 tours
Tour Topics
01. Fundamentals Models, CRUD, queries, aggregations
02. Relationships References, hydration, cross-model queries
03. Safe Models Optimistic locking, conflict resolution
04. Transactions Atomic operations, rollback
05. Mixins Timestamps, soft delete, lifecycle hooks
06. Advanced Bulk ops, indexes, integrity, raw SQL
07. Export/Import CSV, JSON, JSONL
08. Full-Text Search FTS5, boolean queries, ranking
09. Change Tracking Dirty checking, partial updates, diff
10. Database Ops Health checks, stats, vacuum, logging
11. Metrics & Caching Prometheus metrics, query caching, pools

Why sqler?

SQLite is the most deployed database on earth, and JSON1 turns it into a document store. sqler bridges that gap: you get Pydantic validation with document-style flexibility, a fluent query builder that reaches into nested JSON, and real data integrity (optimistic locking, referential policies, transactions) — all in a single file, zero-config database.


Performance

Real numbers from the benchmark suite (22 scenarios, Python 3.12, SQLite 3.50):

Operation Result Context
Bulk insert 84K rows/sec bulk_upsert() at 10K rows
Index speedup 470x 9.4ms → 0.02ms with create_index()
Cache hit 7,000x 14ms → 0.002ms with @cached_query
FTS search 0.28ms Sub-millisecond across all dataset sizes
Bulk vs single 5.2x bulk_upsert vs save() loop at 10K
Lite models 1.3x Dataclass variant vs Pydantic overhead

Index speedup: 470x faster queries

Honest Limitations

  • Single writer — SQLite's architecture. Use bulk_upsert and transactions to batch writes.
  • JSON overhead — Fields are extracted via json_extract(), not native columns. Indexes close the gap.
  • No JOINs — Relationships use reference hydration, not SQL JOINs. Fine for typical document patterns.
  • Memory-bound — Large result sets materialize in Python. Use paginate() and count() to limit.

Full report with 22 charts: benchmarks/results/REPORT.md


Features

Core

  • JSON document models with Pydantic validation (+ lightweight dataclass variant)
  • Fluent query builder: filter, exclude, order_by, paginate, contains, isin, between
  • F() operator for nested fields: F("x")["y"], F(["items"]).any().where(...)
  • Sync and async APIs with matching semantics

Safety & Integrity

  • SQLerSafeModel with optimistic locking (_version + StaleVersionError)
  • Referential integrity: cascade, restrict, set_null delete policies
  • Transaction-aware saves — model.save() inside with db.transaction(): rolls back properly
  • bulk_upsert for efficient batch writes

Developer Experience

  • FTS5 full-text search with ranking and highlights
  • Query caching with TTL and LRU eviction
  • Connection pooling, schema migrations, metrics collection
  • Export/import: CSV, JSON, JSONL (sync + async)
  • 11 interactive marimo tour notebooks
  • Query logging, debug(), explain_query_plan()

When to Use sqler

Good fit:

  • Fast prototyping with real persistence
  • SQLite as embedded app state (Electron, CLI tools, mobile)
  • JSON flexibility + data integrity in one package
  • Pydantic validation on your data layer

Consider alternatives:

  • Need multi-database support → SQLAlchemy
  • Purely relational SQLite workflows → sqlite-utils
  • Complex JOINs at scale → a full ORM with proper relational modeling
  • Write-heavy under contention → PostgreSQL

Showcase

Safe Models — Optimistic Locking

from sqler import SQLerDB, SQLerSafeModel, StaleVersionError

class Account(SQLerSafeModel):
    owner: str
    balance: int

db = SQLerDB.in_memory()
Account.set_db(db)

acc = Account(owner="Ada", balance=100).save()  # _version == 0
acc.balance = 120
acc.save()                                       # _version == 1

# Another process changes the row...
# Your next save detects the conflict:
try:
    acc.balance = 130
    acc.save()
except StaleVersionError:
    acc.refresh()  # reload from DB, then decide

Full tour → · API reference →

Relationships — Hydration & Cross-Filtering

from sqler import SQLerDB, SQLerModel

class Address(SQLerModel):
    city: str
    country: str

class User(SQLerModel):
    name: str
    address: Address | None = None

db = SQLerDB.in_memory()
Address.set_db(db)
User.set_db(db)

home = Address(city="Kyoto", country="JP").save()
user = User(name="Alice", address=home).save()

loaded = User.from_id(user._id)
assert loaded.address.city == "Kyoto"  # auto-hydrated

kyoto_users = User.query().filter(
    User.ref("address").field("city") == "Kyoto"
).all()
assert kyoto_users[0].name == "Alice"

Full tour → · API reference →

Full-Text Search

from sqler import SQLerDB, SQLerModel, FTSIndex

class Article(SQLerModel):
    title: str
    content: str

db = SQLerDB.in_memory()
Article.set_db(db)

Article(title="Python Tips", content="Learn Python fast").save()
Article(title="SQLite Guide", content="Master SQLite queries").save()

fts = FTSIndex(Article, fields=["title", "content"])
fts.create(db)
fts.rebuild()

results = fts.search("Python")        # instant — 0.28ms at any scale
ranked = fts.search_ranked("Python")   # with relevance scores

Full tour → · API reference →


Documentation

Resource Description
API Reference All 46 tested contracts (C01–C46)
Examples Cookbook End-to-end scripts for every feature
Interactive Tours 11 marimo notebooks — run in browser
Benchmark Report 22 scenarios with charts
Changelog Version history

Testing

uv run pytest -q                                       # unit tests
uv run pytest -q -m perf                               # performance benchmarks
uv run python -m benchmarks run --scale small           # full benchmark suite
uv run python -m benchmarks plot                        # generate charts

Contributing

uv run ruff format .                                   # format
uv run ruff check .                                    # lint
uv run pytest -q --cov=src --cov-report=term-missing   # test with coverage

License

MIT

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

sqler-1.2026.2.1.tar.gz (103.4 kB view details)

Uploaded Source

Built Distribution

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

sqler-1.2026.2.1-py3-none-any.whl (131.3 kB view details)

Uploaded Python 3

File details

Details for the file sqler-1.2026.2.1.tar.gz.

File metadata

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

File hashes

Hashes for sqler-1.2026.2.1.tar.gz
Algorithm Hash digest
SHA256 42db53b34a7f769ece2712557a62769fc912a96178d10944c7b40eb76466cc7f
MD5 79e20b321faf89221e7a17e781249dd1
BLAKE2b-256 9df47357c86cd3a67f2cfa119179485e8de8900332ac5e9bbeea0878a1c57ea6

See more details on using hashes here.

Provenance

The following attestation bundles were made for sqler-1.2026.2.1.tar.gz:

Publisher: pypi.yml on gabu-quest/sqler

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

File details

Details for the file sqler-1.2026.2.1-py3-none-any.whl.

File metadata

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

File hashes

Hashes for sqler-1.2026.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fe804f75ed204fadeebc432c06f826915b1b1132b482c1051473200a741cdbac
MD5 4a0279e563cab884b5f5b9a5eb809dc0
BLAKE2b-256 e38e9b2d28891797c88302280d55eae1a4e936f395d5b8c908275efec2299e2e

See more details on using hashes here.

Provenance

The following attestation bundles were made for sqler-1.2026.2.1-py3-none-any.whl:

Publisher: pypi.yml on gabu-quest/sqler

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