What is pyflashdb?
pyflashdb lets you talk to MySQL, PostgreSQL, SQLite and MongoDB with one API — and the same filters, ordering, null handling, upserts and transactions give the same results on each of them. No SQL required, no driver switching, no boilerplate.
from pyflashdb import FlashDB
with FlashDB.from_url("sqlite:///app.db") as flash: # or postgresql://…, mysql://…, mongodb://…
flash.add("users", {"name": "Alice", "age": 25}) # insert
flash.where("users", {"age": {">": 18}}) # filter
flash.update("users", {"name": "Alice"}, {"age": 26}) # update
flash.upsert("users", {"email": "a@x.com", "name": "Alice"}, key="email")
flash.delete("users", {"name": "Alice"}) # delete
Change the URL and nothing else changes.
The claim is tested.
tests/test_parity.pyruns one set of assertions against real MySQL, PostgreSQL, MongoDB and SQLite in CI. If any backend returns different rows, that backend's run fails.
Install
pip install pyflashdb # SQLite works out of the box — no driver needed
pip install "pyflashdb[mysql]" # + MySQL (mysql-connector-python)
pip install "pyflashdb[postgres]" # + PostgreSQL (psycopg2-binary)
pip install "pyflashdb[mongodb]" # + MongoDB (pymongo)
pip install "pyflashdb[all]" # all three drivers
pip install "pyflashdb[pandas]" # + flash.to_df()
The package is pyflashdb and it is imported as pyflashdb. (The old import flash still works in
2.x but is deprecated — see Upgrading.)
Connect
import os
from pyflashdb import FlashDB
flash = FlashDB.from_url(os.environ["DATABASE_URL"]) # passwords stay out of your code
flash = FlashDB.from_env() # same, reads $DATABASE_URL
flash = FlashDB.from_url("postgresql://user:pass@localhost:5432/app", echo=True) # print every query
flash = FlashDB("mysql", {"host": "localhost", "user": "root", "password": "…", "database": "app"})
| Backend | URL | Config keys (dict form) |
|---|---|---|
| MySQL | mysql://user:pass@host:3306/db |
host, port, user, password, database, charset |
| PostgreSQL | postgresql://user:pass@host:5432/db |
host, port, user, password, database |
| SQLite | sqlite:///app.db · sqlite:///:memory: |
database |
| MongoDB | mongodb://user:pass@host:27017/db · mongodb+srv://… |
uri, host, port, user, password, database |
Options (as URL query string or config keys): pool_size (connections, default 1), pool_timeout,
retries, retry_delay, ping_after, connect_args (passed to the driver: ssl, …). A pre-built
pymongo.MongoClient can be supplied as config["client"].
Always use it as a context manager (or call flash.close()):
with FlashDB.from_url(url) as flash:
...
Everyday API
| Method | What it does |
|---|---|
add(table, data) |
Insert one record, returns its primary key |
bulk_insert(table, records) |
Insert many records (atomic on SQL); records may have different keys |
all · select · where · find_one |
Read, with fields, filters, order_by, limit, offset |
count · paginate |
Count / one page + total |
iter(table, batch_size=1000) |
Stream a big table in batches |
update(table, filters, data) |
Update matching records → rows matched |
upsert · upsert_many |
Insert, or update if the key exists |
delete(table, filters) |
Delete matching records |
create_table · drop_table · truncate · add_column · create_index |
Schema |
table_exists · show_tables · describe |
Introspection (same shape on every backend) |
begin · commit · rollback · transaction() |
Transactions |
copy_to(other, tables=[…]) |
Copy tables to another database, across backends |
to_df · to_csv · to_json |
Export |
raw(query, params) |
Escape hatch |
Filters — write once, same result everywhere
flash.where("users", {"age": {">=": 18, "<=": 65}})
flash.where("users", {"role": {"in": ["admin", "mod"]}})
flash.where("users", {"status": {"!=": "banned"}})
flash.where("users", {"email": {"like": "%@gmail.com"}}) # case-SENSITIVE
flash.where("users", {"email": {"ilike": "%@gmail.com"}}) # case-insensitive
flash.where("users", {"deleted_at": None}) # IS NULL / missing
flash.select("users", fields=["name", "age"], order_by=["-age", "name"], limit=10, offset=20)
These rules hold on every backend (they are what the parity suite checks):
likeis case-sensitive and anchored:%= any run of characters,_= one character,\%/\_= literal. Every other character is literal, so the.in%@gmail.comis a dot.ilikeis the case-insensitive form.Nonematches NULL/missing.!=andnot ininclude NULLs (plain SQL would drop them).in []matches nothing;not in []matches everything.order_by="-age"sorts descending; NULLs sort first ascending, last descending.update()returns rows matched, even if a value did not change.- Unknown operators raise
FilterError— nothing user-supplied is ever pasted into SQL, and every table/column name is quoted.
Return types
add() returns |
rows contain | |
|---|---|---|
| MySQL · PostgreSQL · SQLite | int — the new primary key (your own value if you supplied the key; None for a table with no key) |
id |
| MongoDB | str — the new _id as hex |
_id (a str) |
The returned id can be used straight in a filter on every backend:
flash.find_one("users", {"id": new_id}) (SQL) / {"_id": new_id} (Mongo — a 24-hex string is
treated as an ObjectId). Values come back as native Python types everywhere: bool, datetime
(naive UTC), Decimal, and dict/list for json columns.
Upsert
flash.upsert("users", {"email": "a@x.com", "name": "Alice"}, key="email")
flash.upsert("users", data, key=["org_id", "email"], update_fields=["name"]) # only overwrite `name`
flash.upsert_many("users", rows, key="email")
| Backend | Native mechanism |
|---|---|
| MySQL | ON DUPLICATE KEY UPDATE |
| PostgreSQL · SQLite | ON CONFLICT (...) DO UPDATE |
| MongoDB | update_one(..., upsert=True) (bulk_write for many) |
On SQL the key must be backed by a primary key or unique index ({"unique": True} in
create_table). On MySQL the update fires on any unique-key conflict.
Transactions
with flash.transaction(): # commits on success, rolls back on any exception
flash.update("accounts", {"user_id": 1}, {"balance": 400})
flash.update("accounts", {"user_id": 2}, {"balance": 600})
begin() / commit() / rollback() still exist. A transaction() inside another one joins it.
Transactions are per thread.
MongoDB needs a replica set. On a standalone mongod (what most people run locally) begin()
raises a TransactionError explaining how to fix it, instead of failing confusingly. For local
development run a single-node replica set — docker compose up -d in this repository does that — and
connect with ?replicaSet=rs0. Atlas works out of the box.
(MySQL DDL such as TRUNCATE / ALTER TABLE commits implicitly, as in MySQL itself.)
Hooks & plugins
Before/after hooks for every data operation — also on MongoDB, which has no triggers:
@flash.before_insert("users")
def validate(data): # a dict for add(), a list of dicts for bulk_insert()
if not data.get("email"):
raise ValueError("email required") # aborts the insert
@flash.after_insert("users")
def on_created(data, result):
print("new id:", result)
@flash.before_select("orders") # may rewrite the query — e.g. multi-tenant scoping
def scope(payload):
payload["filters"] = {**(payload["filters"] or {}), "tenant_id": 7}
@flash.before_query # sees every statement sent to the database
def log_sql(info):
print(info["backend"], info["statement"], info["params"])
Events: insert, update, delete, select, upsert, count — each with before_* / after_*
("*" hooks every table) — plus before_query / after_query. before hooks can modify the payload
and abort by raising; an exception in an after hook is logged, not raised.
Built-in plugins:
from pyflashdb.plugins import Timestamps, AuditLog
Timestamps(["users", "orders"]).install(flash) # created_at / updated_at maintained for you
AuditLog(["users"]).install(flash) # logs every write to the `pyflashdb.audit` logger
Schema
flash.create_table("users", {
"id": "int", # auto-increment primary key
"email": {"type": "str", "unique": True, "nullable": False},
"name": "str(100)", # VARCHAR(100)
"score": {"type": "float", "default": 0},
"balance": "decimal(10,2)",
"active": {"type": "bool", "default": True},
"meta": "json",
"joined": "datetime",
})
flash.add_column("users", "nickname", {"type": "str", "nullable": True})
flash.create_index("users", ["name", "joined"]) # idempotent; unique=True for a unique index
flash.table_exists("users")
Types: int bigint smallint str text float double decimal bool date datetime
timestamp json blob. Spec keys: type length precision scale primary_key
autoincrement unique nullable default collation. An unrecognised type is passed through only
if it looks like a plain type name (BIGINT, VARCHAR(50)); anything else raises SchemaError.
On MongoDB, create_table applies a $jsonSchema validator (types, nullable: False → required)
and unique indexes; server-side defaults don't exist there and are ignored with a warning.
Large tables, export and copying between databases
for batch in flash.iter("orders", batch_size=1000): # server-side cursor — never loads it all
process(batch)
flash.to_csv("users", "users.csv", fields=["name", "email"])
flash.to_json("users", "users.json", indent=2) # lines=True for JSON Lines
df = flash.to_df("users", filters={"age": {">": 18}}) # pip install "pyflashdb[pandas]"
Copy between databases — "MySQL locally, PostgreSQL in production", made literal:
local = FlashDB.from_url("mysql://root:pw@localhost/app")
prod = FlashDB.from_url(os.environ["PROD_DATABASE_URL"])
local.copy_to(prod, tables=["users", "orders"], batch_size=1000)
# {'users': 1200, 'orders': 8431}
local.copy_to(prod, tables=["users"], mode="upsert", key="email") # or mode="replace"
Missing destination tables are created from the source's columns, types, nullability and primary key (indexes, defaults and foreign keys are not copied). Ids are copied as-is between SQL databases (PostgreSQL counters are re-synced). Between SQL and MongoDB each side keeps its own key.
Production
- Pooling —
pool_size=10(MySQL, PostgreSQL). One shared pool implementation; MongoDB's driver pools by itself. The default (1) is one connection shared safely across threads. - Reconnect & retry — idle connections are health-checked, dead ones replaced. If a connection
drops mid-read the read is retried (
retries=2). Writes are never retried blindly, because the server may already have applied them; the connection is replaced so the next call works. - Observability —
echo=True,before_query/after_queryhooks (with duration and row count), and thepyflashdb.query/pyflashdb.audit/pyflashdb.hooksloggers. - Typed — full type hints and a
py.typedmarker, so IDE completion and mypy work. raw()—flash.raw("SELECT * FROM users WHERE age > %s", [18]). The placeholder is%son every SQL backend, SQLite included (write%%for a literal percent sign when passing params). Statements that return rows give alist[dict](possibly empty); others give the row count. MongoDB:flash.raw({"age": {"$gt": 18}}, table="users")orflash.raw("dbStats").
Known differences
pyflashdb removes the differences that can be removed. These remain, by nature of the databases:
- Ids:
intvsstr(above). MongoDB documents contain only the fields you stored — a missing field is not turned intoNone. - String collation: tables created by
create_tableuse a binary collation on MySQL, so=,INandLIKEare case-sensitive like everywhere else. On tables you created yourself,=follows the column's own collation (like/ilikeare correct regardless). Sort order of non-ASCII text and non-ASCII case folding forilikefollow each database's locale. - SQLite stores
decimalas a numeric value (precision beyond ~15 digits is not guaranteed) and has a single writer, so it uses one connection. - MongoDB: transactions need a replica set;
bulk_insertis not atomic outside a transaction; no column defaults. - MySQL:
upsertfires on any unique key;JSONcolumns need MySQL ≥ 5.7.
Upgrading from 1.x
2.0 is a major release. What to change:
- Import
pyflashdb:from pyflashdb import FlashDB.import flashstill works for now but warns and will be removed in a later release. - Python ≥ 3.9 (3.8 is end-of-life).
before_*hooks can now abort (raise to cancel the operation) — in 1.x exceptions were swallowed and printed.after_*hook errors are logged vialogging, no longerprinted.describe()returns[{"name", "type", "nullable", "default", "primary_key"}]on every backend.!=/not innow include NULLs on SQL (as MongoDB always did);likeis case-sensitive on MySQL (it was case-insensitive);order_by="-x"now really sorts descending on SQL;update()returns rows matched (MySQL used to return rows changed); Mongoselect(fields=…)no longer includes_id.- Unknown filter operators (including raw
$gt-style operators on Mongo) raiseFilterError. floatcolumns are now double precision (DOUBLE/DOUBLE PRECISION), andtimestampcolumns on MySQL areDATETIME(6).- PostgreSQL
truncate()no longer cascades into other tables. create_index()now works on every backend (it was MongoDB-only).
Full list: Changelog.
How pyflashdb compares
| pyflashdb | SQLAlchemy | Django ORM | Raw drivers | |
|---|---|---|---|---|
| MySQL / PostgreSQL | ✅ | ✅ | ✅ | ✅ |
| SQLite | ✅ | ✅ | ✅ | ✅ |
| MongoDB | ✅ | ❌ | ❌ | ✅ |
| Same API across all DBs | ✅ | ❌ | ❌ | ❌ |
| Copy tables between DBs | ✅ | ❌ | ❌ | ❌ |
Development
pip install -e ".[all,dev]"
pytest # SQLite + unit tests; MySQL/PostgreSQL/Mongo are skipped unless configured
docker compose up -d --wait # start MySQL, PostgreSQL and a MongoDB replica set, then export:
export PYFLASHDB_TEST_MYSQL_URL=mysql://root:flash@127.0.0.1:3306/flash_test
export PYFLASHDB_TEST_POSTGRES_URL=postgresql://postgres:flash@127.0.0.1:5432/flash_test
export PYFLASHDB_TEST_MONGODB_URL='mongodb://127.0.0.1:27017/flash_test?replicaSet=rs0'
pytest -ra # the full parity suite
Documentation
📄 Full Documentation (PDF, v2.0.0) — a 39-page manual covering every method, the cross-database behavior contract, production use, upgrading from 1.x, and the full list of what's new in 2.0 (last chapter).
Links
- PyPI: https://pypi.org/project/pyflashdb/
- Issues: https://github.com/AmanDev17/pyflashdb/issues
- Changelog: https://github.com/AmanDev17/pyflashdb/blob/main/Changelog.md
pyflashdb v2.0.0 · MIT License · Python 3.9+
One API. Any database. Same behavior.
Made possible because of Komal
Metadata
Release files for pyflashdb 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pyflashdb-2.0.0.tar.gz | 82.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyflashdb-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 148.8 kB
Release files / pyflashdb-2.0.0.tar.gz
| Download URL | pyflashdb-2.0.0.tar.gz |
|---|---|
| Size | 82.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2054852fb2a9575f7219cce47e73fb10c300319f57a45fcb7d62c5c203b33fb6
|
|
BLAKE2b-256 checksum How to use checksums |
aeb1623867fa173096236d025307978a017fc2f9c24659112a32f99dc1c47148
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / pyflashdb-2.0.0-py3-none-any.whl
| Download URL | pyflashdb-2.0.0-py3-none-any.whl |
|---|---|
| Size | 66.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
97d58336dbbd1949120c9d0b6ea6d95536ba8c23e32c3d394e9932c2952f7e4f
|
|
BLAKE2b-256 checksum How to use checksums |
164bcd476990f073212f7af9204380cb12a604f25fb47cdc81f60995e9bbc657
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Security for pyflashdb 2.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.