pyflashdb 1.0.4__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. pyflashdb-2.0.0/Changelog.md +135 -0
  2. {pyflashdb-1.0.4 → pyflashdb-2.0.0}/LICENSE +20 -20
  3. pyflashdb-2.0.0/MANIFEST.in +7 -0
  4. pyflashdb-2.0.0/PKG-INFO +440 -0
  5. pyflashdb-2.0.0/README.md +393 -0
  6. pyflashdb-2.0.0/docker-compose.yml +52 -0
  7. pyflashdb-2.0.0/examples/quickstart.py +80 -0
  8. pyflashdb-2.0.0/flash/__init__.py +37 -0
  9. pyflashdb-2.0.0/pyflashdb/__init__.py +44 -0
  10. pyflashdb-2.0.0/pyflashdb/_like.py +83 -0
  11. pyflashdb-2.0.0/pyflashdb/_util.py +58 -0
  12. pyflashdb-2.0.0/pyflashdb/adapters/__init__.py +6 -0
  13. pyflashdb-2.0.0/pyflashdb/adapters/base.py +141 -0
  14. pyflashdb-2.0.0/pyflashdb/adapters/mongo_adapter.py +718 -0
  15. pyflashdb-2.0.0/pyflashdb/adapters/mysql_adapter.py +242 -0
  16. pyflashdb-2.0.0/pyflashdb/adapters/postgres_adapter.py +243 -0
  17. pyflashdb-2.0.0/pyflashdb/adapters/sql.py +597 -0
  18. pyflashdb-2.0.0/pyflashdb/adapters/sqlite_adapter.py +287 -0
  19. pyflashdb-2.0.0/pyflashdb/core.py +762 -0
  20. pyflashdb-2.0.0/pyflashdb/dialects.py +345 -0
  21. pyflashdb-2.0.0/pyflashdb/exceptions.py +43 -0
  22. pyflashdb-2.0.0/pyflashdb/export.py +117 -0
  23. pyflashdb-2.0.0/pyflashdb/filters.py +262 -0
  24. pyflashdb-2.0.0/pyflashdb/plugins.py +135 -0
  25. pyflashdb-2.0.0/pyflashdb/pool.py +118 -0
  26. pyflashdb-2.0.0/pyflashdb/schema.py +254 -0
  27. pyflashdb-2.0.0/pyflashdb/triggers.py +242 -0
  28. pyflashdb-2.0.0/pyflashdb/urls.py +90 -0
  29. pyflashdb-2.0.0/pyflashdb.egg-info/PKG-INFO +440 -0
  30. pyflashdb-2.0.0/pyflashdb.egg-info/SOURCES.txt +43 -0
  31. pyflashdb-2.0.0/pyflashdb.egg-info/requires.txt +25 -0
  32. pyflashdb-2.0.0/pyflashdb.egg-info/top_level.txt +2 -0
  33. pyflashdb-2.0.0/pyproject.toml +85 -0
  34. pyflashdb-2.0.0/tests/conftest.py +61 -0
  35. pyflashdb-2.0.0/tests/support.py +73 -0
  36. pyflashdb-2.0.0/tests/test_copy.py +63 -0
  37. pyflashdb-2.0.0/tests/test_dialects_schema.py +225 -0
  38. pyflashdb-2.0.0/tests/test_filters.py +190 -0
  39. pyflashdb-2.0.0/tests/test_mongo_unit.py +120 -0
  40. pyflashdb-2.0.0/tests/test_parity.py +443 -0
  41. pyflashdb-2.0.0/tests/test_sqlite_and_internals.py +300 -0
  42. pyflashdb-2.0.0/tests/test_urls_and_shim.py +88 -0
  43. pyflashdb-1.0.4/MANIFEST.in +0 -6
  44. pyflashdb-1.0.4/PKG-INFO +0 -236
  45. pyflashdb-1.0.4/README.md +0 -222
  46. pyflashdb-1.0.4/flash/__init__.py +0 -33
  47. pyflashdb-1.0.4/flash/adapters/mongo_adapter.py +0 -247
  48. pyflashdb-1.0.4/flash/adapters/mysql_adapter.py +0 -237
  49. pyflashdb-1.0.4/flash/adapters/postgres_adapter.py +0 -258
  50. pyflashdb-1.0.4/flash/core.py +0 -323
  51. pyflashdb-1.0.4/flash/exceptions.py +0 -48
  52. pyflashdb-1.0.4/flash/filters.py +0 -217
  53. pyflashdb-1.0.4/flash/triggers.py +0 -191
  54. pyflashdb-1.0.4/pyflashdb.egg-info/PKG-INFO +0 -236
  55. pyflashdb-1.0.4/pyflashdb.egg-info/SOURCES.txt +0 -18
  56. pyflashdb-1.0.4/pyflashdb.egg-info/top_level.txt +0 -3
  57. pyflashdb-1.0.4/pyproject.toml +0 -17
  58. pyflashdb-1.0.4/setup.py +0 -40
  59. /pyflashdb-1.0.4/flash/adapters/__init__.py → /pyflashdb-2.0.0/pyflashdb/py.typed +0 -0
  60. {pyflashdb-1.0.4 → pyflashdb-2.0.0}/pyflashdb.egg-info/dependency_links.txt +0 -0
  61. {pyflashdb-1.0.4 → pyflashdb-2.0.0}/setup.cfg +0 -0
@@ -0,0 +1,135 @@
1
+ # Changelog
2
+
3
+ ## v2.0.0 — Same behavior, any database
4
+
5
+ The headline of 2.0 is the library's own promise — *same code, same behavior, any database* — made
6
+ true, and tested: one parity suite (`tests/test_parity.py`) now runs identical assertions against
7
+ MySQL, PostgreSQL, SQLite and MongoDB in CI.
8
+
9
+ ### Breaking changes
10
+
11
+ - **Import name is now `pyflashdb`** (`from pyflashdb import FlashDB`). `flash` was too generic a
12
+ top-level name. `import flash` still works as a **deprecated alias** (emits `DeprecationWarning`,
13
+ shares the same classes and exceptions) and will be removed in a later release. The pip package name
14
+ is unchanged.
15
+ - **Python ≥ 3.9** (3.8 is end-of-life).
16
+ - **`before_*` hooks can abort an operation** by raising (as the README's validation example always
17
+ implied). Previously every hook exception was swallowed and `print`ed. Exceptions in `after_*`
18
+ hooks are now logged through `logging` (`pyflashdb.hooks`) instead of `print`ed.
19
+ - **Backend behaviour now matches across databases** (these change results on some backends):
20
+ - `like` is case-sensitive everywhere (it was case-insensitive on MySQL, and unanchored on MongoDB).
21
+ Use the new `ilike` for case-insensitive matching.
22
+ - `!=` and `not in` include rows where the field is NULL on SQL, as MongoDB always did.
23
+ - `order_by="-field"` really sorts descending on SQL (it silently sorted ascending).
24
+ NULLs sort first ascending / last descending on every backend.
25
+ - `update()` returns rows *matched* on every backend (MySQL returned rows *changed*, MongoDB
26
+ `modified_count`).
27
+ - MongoDB `select(fields=[...])` no longer returns `_id` unless asked.
28
+ - `limit=0` returns no rows on every backend (MongoDB treated it as "no limit").
29
+ - Unknown filter operators — including Mongo-style `{"$gt": …}` — raise `FilterError`; they used to
30
+ be pasted into the SQL statement.
31
+ - **`describe()`** returns `[{"name", "type", "nullable", "default", "primary_key"}]` on every backend
32
+ instead of each driver's native shape.
33
+ - **New tables**: `float` is now double precision (`DOUBLE` / `DOUBLE PRECISION`; it was single
34
+ precision `FLOAT` / `REAL` and lost precision on round trips); `timestamp` is `DATETIME(6)` on MySQL;
35
+ text columns on MySQL use a binary collation so `=` is case-sensitive like on the other backends;
36
+ the primary-key column only auto-increments when it is an integer.
37
+ - **PostgreSQL `truncate()`** no longer uses `CASCADE` (it silently emptied other tables). It still
38
+ restarts identity counters.
39
+ - Records passed to **`bulk_insert`** are matched to columns by name (values used to be paired by
40
+ position, which corrupted data when dicts had different key order).
41
+
42
+ ### Added
43
+
44
+ - **SQLite backend** — `FlashDB.from_url("sqlite:///app.db")`. Zero setup, standard library only.
45
+ - **`upsert()` / `upsert_many()`** — `ON DUPLICATE KEY UPDATE` (MySQL), `ON CONFLICT … DO UPDATE`
46
+ (PostgreSQL, SQLite), `update_one(…, upsert=True)` (MongoDB). `update_fields` limits what is overwritten.
47
+ - **`ilike`** operator; `like` escapes (`\%`, `\_`); the `.` in `"%@gmail.com"` is a literal dot on MongoDB.
48
+ - **`copy_to()`** — copy tables to another FlashDB, across backends, in batches (`append` / `replace` /
49
+ `upsert`); creates missing destination tables from the source schema.
50
+ - **`from_url()` / `from_env()`** and connection URLs (`mysql://`, `postgresql://`, `sqlite://`,
51
+ `mongodb://`, `mongodb+srv://`); **`echo=True`**.
52
+ - **`transaction()`** context manager (commit on success, rollback on exception; nestable).
53
+ - **MongoDB transactions work** and fail clearly: `begin()` detects a standalone server and raises a
54
+ `TransactionError` explaining how to run a replica set. (In 1.x the session was never passed to the
55
+ operations, so `rollback()` did nothing.)
56
+ - **`iter()`** — stream a table in batches via server-side cursors (MySQL, PostgreSQL) / cursors (MongoDB, SQLite).
57
+ - **Export**: `to_df()` (`pip install pyflashdb[pandas]`), `to_csv()`, `to_json()` (array or JSON Lines).
58
+ - **Connection pooling** (`pool_size`), **automatic reconnect** with read retries (`retries`,
59
+ `retry_delay`, `ping_after`), thread-safe use, per-thread transactions.
60
+ - **Richer schema**: `float`, `double`, `decimal(p,s)`, `bool`, `date`, `datetime`, `json`, `blob`, `str(n)`;
61
+ constraints `primary_key`, `unique`, `nullable`, `default`, `collation`; `create_index()` on every
62
+ backend, `table_exists()`, `add_column()`.
63
+ - **Hooks**: `before_*` / `after_*` for `select`, `upsert` and `count` as well as insert/update/delete;
64
+ `"*"` wildcard table; global `before_query` / `after_query` (statement, params, duration, row count);
65
+ `before_select` may rewrite the query.
66
+ - **Built-in plugins**: `Timestamps` (created_at / updated_at) and `AuditLog`.
67
+ - **`raw(sql, params)`** with a single `%s` placeholder style on all SQL backends (SQLite included).
68
+ - Full **type hints** and a **`py.typed`** marker.
69
+ - Parity test suite, cross-database copy tests, unit tests, GitHub Actions CI (MySQL, PostgreSQL,
70
+ MongoDB replica set, SQLite; Python 3.9–3.13), and a `docker-compose.yml` for local runs.
71
+ - **New manual** — `pyflashdb_v_2.0.0_README.pdf` replaces the 1.0.4 manual (same design): all 2.0 features,
72
+ the cross-database behavior contract, production use, an "Upgrading from 1.x" guide, and the 2.0 release notes.
73
+
74
+ ### Fixed
75
+
76
+ - **SQL injection paths closed**: table, column and index names are always quoted; filter operators
77
+ and column types are validated instead of interpolated; `LIMIT`/`OFFSET` must be integers.
78
+ - `OFFSET` without `LIMIT` now works on MySQL and SQLite.
79
+ - `raw()` on SQL returned the row *count* instead of `[]` for a SELECT with no rows.
80
+ - PostgreSQL `add()` assumed the key column was called `id`; it now looks the key up (and returns
81
+ `None` for key-less tables instead of failing).
82
+ - MySQL `TINYINT(1)` columns come back as `bool` and `JSON` columns as Python objects; PostgreSQL
83
+ `bytea` comes back as `bytes`; MongoDB `Decimal128` as `Decimal`; timezone-aware datetimes are stored
84
+ as naive UTC on every backend.
85
+ - `update()` with no fields, and empty `IN ()` lists, no longer generate invalid SQL/queries.
86
+ - `bulk_insert` / `upsert_many` on SQL are atomic.
87
+
88
+ ### Packaging
89
+
90
+ - Metadata moved to `pyproject.toml` (PEP 621); `setup.py` removed. The previous
91
+ `build-backend = "setuptools.backends.legacy:build"` did not exist, so building from source failed.
92
+ - Optional extras: `mysql`, `postgres`, `mongodb`, `pandas`, `all`, `dev`. SQLite needs no extra.
93
+ - Release workflow publishes to PyPI via trusted publishing when a GitHub release is published.
94
+
95
+ ### Not in 2.0
96
+
97
+ - An `AsyncFlashDB` (asyncpg / aiomysql / motor) is planned for a later release.
98
+
99
+ ## v1.0.4 — Bug Fix Release
100
+
101
+ ### Fixed
102
+
103
+ - **`FlashConnectionError` rename** (`flash/__init__.py`, `flash/exceptions.py`)
104
+ `ConnectionError` renamed to `FlashConnectionError` throughout the public API.
105
+ The old name silently shadowed Python's built-in `ConnectionError`, breaking
106
+ any code that caught real OS-level networking errors after importing from Flash.
107
+ A private backwards-compatibility alias remains in `flash.exceptions` only.
108
+
109
+ - **PostgreSQL `add()` return type** (`flash/adapters/postgres_adapter.py`)
110
+ `add()` now returns an `int` primary key (consistent with the MySQL adapter)
111
+ instead of the entire inserted row as a dict. Changed `RETURNING *` to
112
+ `RETURNING id` (or the configured `primary_key`).
113
+
114
+ - **`like` filter on MongoDB** (`flash/filters.py`)
115
+ SQL LIKE wildcards (`%` → any chars, `_` → one char) are now correctly
116
+ converted to regex equivalents (`.*` and `.`) and regex meta-characters in
117
+ the pattern are escaped before the value is sent to MongoDB's `$regex`.
118
+ Previously `%` was passed verbatim and matched nothing.
119
+
120
+ - **`__version__` corrected** (`flash/__init__.py`)
121
+ Updated from `"1.0.0"` to `"1.0.4"` to match the PyPI release.
122
+
123
+ - **All exceptions exported** (`flash/__init__.py`)
124
+ `TransactionError`, `SchemaError`, and `UnsupportedDatabaseError` are now
125
+ exported directly from `flash` in addition to `flash.exceptions`.
126
+
127
+ - **`raw()` routing fixed** (`flash/core.py`)
128
+ MongoDB's `table` keyword argument is now correctly forwarded to the adapter.
129
+ SQL adapters receive `(sql, params)`; the MongoDB adapter receives
130
+ `(command, table=table)`. Previously `table` was silently ignored for all
131
+ database types.
132
+
133
+ ## v1.0.3
134
+
135
+ - Initial public PyPI release.
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Aman
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Aman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
@@ -0,0 +1,7 @@
1
+ include LICENSE README.md Changelog.md
2
+ include pyflashdb/py.typed
3
+ recursive-include tests *.py
4
+ recursive-include examples *.py
5
+ include docker-compose.yml
6
+ prune .github
7
+ global-exclude *.pyc __pycache__ *.pdf
@@ -0,0 +1,440 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyflashdb
3
+ Version: 2.0.0
4
+ Summary: One API for MySQL, PostgreSQL, SQLite and MongoDB — same code, same behavior, any database.
5
+ Author: Aman Singh
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AmanDev17/pyflashdb
8
+ Project-URL: Issues, https://github.com/AmanDev17/pyflashdb/issues
9
+ Project-URL: Changelog, https://github.com/AmanDev17/pyflashdb/blob/main/Changelog.md
10
+ Project-URL: PyPI, https://pypi.org/project/pyflashdb/
11
+ Keywords: database,mysql,postgresql,sqlite,mongodb,crud,orm,unified,upsert,pyflashdb
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Database
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: mysql
28
+ Requires-Dist: mysql-connector-python>=8.0; extra == "mysql"
29
+ Provides-Extra: postgres
30
+ Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
31
+ Provides-Extra: mongodb
32
+ Requires-Dist: pymongo>=4.0; extra == "mongodb"
33
+ Provides-Extra: pandas
34
+ Requires-Dist: pandas>=1.5; extra == "pandas"
35
+ Provides-Extra: all
36
+ Requires-Dist: mysql-connector-python>=8.0; extra == "all"
37
+ Requires-Dist: psycopg2-binary>=2.9; extra == "all"
38
+ Requires-Dist: pymongo>=4.0; extra == "all"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=7; extra == "dev"
41
+ Requires-Dist: pandas>=1.5; extra == "dev"
42
+ Requires-Dist: mypy>=1.8; extra == "dev"
43
+ Requires-Dist: ruff>=0.4; extra == "dev"
44
+ Requires-Dist: build; extra == "dev"
45
+ Requires-Dist: twine; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ <div align="center">
49
+
50
+ <br/>
51
+
52
+ # ⚡ pyflashdb
53
+
54
+ **Same code. Same behavior. Any database.**
55
+
56
+ [![CI](https://github.com/AmanDev17/pyflashdb/actions/workflows/ci.yml/badge.svg)](https://github.com/AmanDev17/pyflashdb/actions/workflows/ci.yml)
57
+ [![Parity tested](https://img.shields.io/badge/parity%20tested-MySQL%20%7C%20PostgreSQL%20%7C%20SQLite%20%7C%20MongoDB-10B981?style=flat-square)](https://github.com/AmanDev17/pyflashdb/blob/main/tests/test_parity.py)
58
+ [![PyPI](https://img.shields.io/pypi/v/pyflashdb?style=flat-square&logo=pypi&logoColor=white&color=E94560)](https://pypi.org/project/pyflashdb/)
59
+ [![Python](https://img.shields.io/pypi/pyversions/pyflashdb?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
60
+ [![License: MIT](https://img.shields.io/badge/license-MIT-10B981?style=flat-square)](https://github.com/AmanDev17/pyflashdb/blob/main/LICENSE)
61
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyflashdb?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/pyflashdb)
62
+
63
+ <br/>
64
+
65
+ </div>
66
+
67
+ ---
68
+
69
+ ## What is pyflashdb?
70
+
71
+ pyflashdb lets you talk to **MySQL**, **PostgreSQL**, **SQLite** and **MongoDB** with one API — and
72
+ the *same* filters, ordering, null handling, upserts and transactions give the *same* results on
73
+ each of them. No SQL required, no driver switching, no boilerplate.
74
+
75
+ ```python
76
+ from pyflashdb import FlashDB
77
+
78
+ with FlashDB.from_url("sqlite:///app.db") as flash: # or postgresql://…, mysql://…, mongodb://…
79
+ flash.add("users", {"name": "Alice", "age": 25}) # insert
80
+ flash.where("users", {"age": {">": 18}}) # filter
81
+ flash.update("users", {"name": "Alice"}, {"age": 26}) # update
82
+ flash.upsert("users", {"email": "a@x.com", "name": "Alice"}, key="email")
83
+ flash.delete("users", {"name": "Alice"}) # delete
84
+ ```
85
+
86
+ Change the URL and nothing else changes.
87
+
88
+ > **The claim is tested.** [`tests/test_parity.py`](https://github.com/AmanDev17/pyflashdb/blob/main/tests/test_parity.py)
89
+ > runs one set of assertions against real MySQL, PostgreSQL, MongoDB and SQLite in CI. If any
90
+ > backend returns different rows, that backend's run fails.
91
+
92
+ ---
93
+
94
+ ## Install
95
+
96
+ ```bash
97
+ pip install pyflashdb # SQLite works out of the box — no driver needed
98
+ pip install "pyflashdb[mysql]" # + MySQL (mysql-connector-python)
99
+ pip install "pyflashdb[postgres]" # + PostgreSQL (psycopg2-binary)
100
+ pip install "pyflashdb[mongodb]" # + MongoDB (pymongo)
101
+ pip install "pyflashdb[all]" # all three drivers
102
+ pip install "pyflashdb[pandas]" # + flash.to_df()
103
+ ```
104
+
105
+ The package is `pyflashdb` and it is imported as `pyflashdb`. (The old `import flash` still works in
106
+ 2.x but is deprecated — see [Upgrading](#upgrading-from-1x).)
107
+
108
+ ---
109
+
110
+ ## Connect
111
+
112
+ ```python
113
+ import os
114
+ from pyflashdb import FlashDB
115
+
116
+ flash = FlashDB.from_url(os.environ["DATABASE_URL"]) # passwords stay out of your code
117
+ flash = FlashDB.from_env() # same, reads $DATABASE_URL
118
+ flash = FlashDB.from_url("postgresql://user:pass@localhost:5432/app", echo=True) # print every query
119
+
120
+ flash = FlashDB("mysql", {"host": "localhost", "user": "root", "password": "…", "database": "app"})
121
+ ```
122
+
123
+ | Backend | URL | Config keys (dict form) |
124
+ | ---------- | ------------------------------------------------ | ----------------------------------------------- |
125
+ | MySQL | `mysql://user:pass@host:3306/db` | host, port, user, password, database, charset |
126
+ | PostgreSQL | `postgresql://user:pass@host:5432/db` | host, port, user, password, database |
127
+ | SQLite | `sqlite:///app.db` · `sqlite:///:memory:` | database |
128
+ | MongoDB | `mongodb://user:pass@host:27017/db` · `mongodb+srv://…` | uri, host, port, user, password, database |
129
+
130
+ Options (as URL query string or config keys): `pool_size` (connections, default 1), `pool_timeout`,
131
+ `retries`, `retry_delay`, `ping_after`, `connect_args` (passed to the driver: ssl, …). A pre-built
132
+ `pymongo.MongoClient` can be supplied as `config["client"]`.
133
+
134
+ Always use it as a context manager (or call `flash.close()`):
135
+
136
+ ```python
137
+ with FlashDB.from_url(url) as flash:
138
+ ...
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Everyday API
144
+
145
+ | Method | What it does |
146
+ | --- | --- |
147
+ | `add(table, data)` | Insert one record, returns its primary key |
148
+ | `bulk_insert(table, records)` | Insert many records (atomic on SQL); records may have different keys |
149
+ | `all` · `select` · `where` · `find_one` | Read, with `fields`, `filters`, `order_by`, `limit`, `offset` |
150
+ | `count` · `paginate` | Count / one page + total |
151
+ | `iter(table, batch_size=1000)` | Stream a big table in batches |
152
+ | `update(table, filters, data)` | Update matching records → rows *matched* |
153
+ | `upsert` · `upsert_many` | Insert, or update if the key exists |
154
+ | `delete(table, filters)` | Delete matching records |
155
+ | `create_table` · `drop_table` · `truncate` · `add_column` · `create_index` | Schema |
156
+ | `table_exists` · `show_tables` · `describe` | Introspection (same shape on every backend) |
157
+ | `begin` · `commit` · `rollback` · `transaction()` | Transactions |
158
+ | `copy_to(other, tables=[…])` | Copy tables to another database, across backends |
159
+ | `to_df` · `to_csv` · `to_json` | Export |
160
+ | `raw(query, params)` | Escape hatch |
161
+
162
+ ### Filters — write once, same result everywhere
163
+
164
+ ```python
165
+ flash.where("users", {"age": {">=": 18, "<=": 65}})
166
+ flash.where("users", {"role": {"in": ["admin", "mod"]}})
167
+ flash.where("users", {"status": {"!=": "banned"}})
168
+ flash.where("users", {"email": {"like": "%@gmail.com"}}) # case-SENSITIVE
169
+ flash.where("users", {"email": {"ilike": "%@gmail.com"}}) # case-insensitive
170
+ flash.where("users", {"deleted_at": None}) # IS NULL / missing
171
+ flash.select("users", fields=["name", "age"], order_by=["-age", "name"], limit=10, offset=20)
172
+ ```
173
+
174
+ These rules hold on **every** backend (they are what the parity suite checks):
175
+
176
+ * `like` is **case-sensitive** and **anchored**: `%` = any run of characters, `_` = one character,
177
+ `\%` / `\_` = literal. Every other character is literal, so the `.` in `%@gmail.com` is a dot.
178
+ `ilike` is the case-insensitive form.
179
+ * `None` matches NULL/missing. `!=` and `not in` **include** NULLs (plain SQL would drop them).
180
+ `in []` matches nothing; `not in []` matches everything.
181
+ * `order_by="-age"` sorts descending; NULLs sort first ascending, last descending.
182
+ * `update()` returns rows **matched**, even if a value did not change.
183
+ * Unknown operators raise `FilterError` — nothing user-supplied is ever pasted into SQL, and every
184
+ table/column name is quoted.
185
+
186
+ ### Return types
187
+
188
+ | | `add()` returns | rows contain |
189
+ | --- | --- | --- |
190
+ | MySQL · PostgreSQL · SQLite | `int` — the new primary key (your own value if you supplied the key; `None` for a table with no key) | `id` |
191
+ | MongoDB | `str` — the new `_id` as hex | `_id` (a `str`) |
192
+
193
+ The returned id can be used straight in a filter on every backend:
194
+ `flash.find_one("users", {"id": new_id})` (SQL) / `{"_id": new_id}` (Mongo — a 24-hex string is
195
+ treated as an ObjectId). Values come back as native Python types everywhere: `bool`, `datetime`
196
+ (naive UTC), `Decimal`, and `dict`/`list` for `json` columns.
197
+
198
+ ---
199
+
200
+ ## Upsert
201
+
202
+ ```python
203
+ flash.upsert("users", {"email": "a@x.com", "name": "Alice"}, key="email")
204
+ flash.upsert("users", data, key=["org_id", "email"], update_fields=["name"]) # only overwrite `name`
205
+ flash.upsert_many("users", rows, key="email")
206
+ ```
207
+
208
+ | Backend | Native mechanism |
209
+ | --- | --- |
210
+ | MySQL | `ON DUPLICATE KEY UPDATE` |
211
+ | PostgreSQL · SQLite | `ON CONFLICT (...) DO UPDATE` |
212
+ | MongoDB | `update_one(..., upsert=True)` (`bulk_write` for many) |
213
+
214
+ On SQL the `key` must be backed by a primary key or unique index (`{"unique": True}` in
215
+ `create_table`). On MySQL the update fires on *any* unique-key conflict.
216
+
217
+ ---
218
+
219
+ ## Transactions
220
+
221
+ ```python
222
+ with flash.transaction(): # commits on success, rolls back on any exception
223
+ flash.update("accounts", {"user_id": 1}, {"balance": 400})
224
+ flash.update("accounts", {"user_id": 2}, {"balance": 600})
225
+ ```
226
+
227
+ `begin()` / `commit()` / `rollback()` still exist. A `transaction()` inside another one joins it.
228
+ Transactions are per thread.
229
+
230
+ **MongoDB needs a replica set.** On a standalone `mongod` (what most people run locally) `begin()`
231
+ raises a `TransactionError` explaining how to fix it, instead of failing confusingly. For local
232
+ development run a single-node replica set — `docker compose up -d` in this repository does that — and
233
+ connect with `?replicaSet=rs0`. Atlas works out of the box.
234
+
235
+ (MySQL DDL such as `TRUNCATE` / `ALTER TABLE` commits implicitly, as in MySQL itself.)
236
+
237
+ ---
238
+
239
+ ## Hooks & plugins
240
+
241
+ Before/after hooks for every data operation — also on MongoDB, which has no triggers:
242
+
243
+ ```python
244
+ @flash.before_insert("users")
245
+ def validate(data): # a dict for add(), a list of dicts for bulk_insert()
246
+ if not data.get("email"):
247
+ raise ValueError("email required") # aborts the insert
248
+
249
+ @flash.after_insert("users")
250
+ def on_created(data, result):
251
+ print("new id:", result)
252
+
253
+ @flash.before_select("orders") # may rewrite the query — e.g. multi-tenant scoping
254
+ def scope(payload):
255
+ payload["filters"] = {**(payload["filters"] or {}), "tenant_id": 7}
256
+
257
+ @flash.before_query # sees every statement sent to the database
258
+ def log_sql(info):
259
+ print(info["backend"], info["statement"], info["params"])
260
+ ```
261
+
262
+ Events: `insert`, `update`, `delete`, `select`, `upsert`, `count` — each with `before_*` / `after_*`
263
+ (`"*"` hooks every table) — plus `before_query` / `after_query`. `before` hooks can modify the payload
264
+ and **abort** by raising; an exception in an `after` hook is logged, not raised.
265
+
266
+ Built-in plugins:
267
+
268
+ ```python
269
+ from pyflashdb.plugins import Timestamps, AuditLog
270
+
271
+ Timestamps(["users", "orders"]).install(flash) # created_at / updated_at maintained for you
272
+ AuditLog(["users"]).install(flash) # logs every write to the `pyflashdb.audit` logger
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Schema
278
+
279
+ ```python
280
+ flash.create_table("users", {
281
+ "id": "int", # auto-increment primary key
282
+ "email": {"type": "str", "unique": True, "nullable": False},
283
+ "name": "str(100)", # VARCHAR(100)
284
+ "score": {"type": "float", "default": 0},
285
+ "balance": "decimal(10,2)",
286
+ "active": {"type": "bool", "default": True},
287
+ "meta": "json",
288
+ "joined": "datetime",
289
+ })
290
+ flash.add_column("users", "nickname", {"type": "str", "nullable": True})
291
+ flash.create_index("users", ["name", "joined"]) # idempotent; unique=True for a unique index
292
+ flash.table_exists("users")
293
+ ```
294
+
295
+ Types: `int` `bigint` `smallint` `str` `text` `float` `double` `decimal` `bool` `date` `datetime`
296
+ `timestamp` `json` `blob`. Spec keys: `type` `length` `precision` `scale` `primary_key`
297
+ `autoincrement` `unique` `nullable` `default` `collation`. An unrecognised type is passed through only
298
+ if it looks like a plain type name (`BIGINT`, `VARCHAR(50)`); anything else raises `SchemaError`.
299
+
300
+ On MongoDB, `create_table` applies a `$jsonSchema` validator (types, `nullable: False` → required)
301
+ and unique indexes; server-side `default`s don't exist there and are ignored with a warning.
302
+
303
+ ---
304
+
305
+ ## Large tables, export and copying between databases
306
+
307
+ ```python
308
+ for batch in flash.iter("orders", batch_size=1000): # server-side cursor — never loads it all
309
+ process(batch)
310
+
311
+ flash.to_csv("users", "users.csv", fields=["name", "email"])
312
+ flash.to_json("users", "users.json", indent=2) # lines=True for JSON Lines
313
+ df = flash.to_df("users", filters={"age": {">": 18}}) # pip install "pyflashdb[pandas]"
314
+ ```
315
+
316
+ **Copy between databases** — "MySQL locally, PostgreSQL in production", made literal:
317
+
318
+ ```python
319
+ local = FlashDB.from_url("mysql://root:pw@localhost/app")
320
+ prod = FlashDB.from_url(os.environ["PROD_DATABASE_URL"])
321
+
322
+ local.copy_to(prod, tables=["users", "orders"], batch_size=1000)
323
+ # {'users': 1200, 'orders': 8431}
324
+ local.copy_to(prod, tables=["users"], mode="upsert", key="email") # or mode="replace"
325
+ ```
326
+
327
+ Missing destination tables are created from the source's columns, types, nullability and primary
328
+ key (indexes, defaults and foreign keys are not copied). Ids are copied as-is between SQL databases
329
+ (PostgreSQL counters are re-synced). Between SQL and MongoDB each side keeps its own key.
330
+
331
+ ---
332
+
333
+ ## Production
334
+
335
+ * **Pooling** — `pool_size=10` (MySQL, PostgreSQL). One shared pool implementation; MongoDB's driver
336
+ pools by itself. The default (`1`) is one connection shared safely across threads.
337
+ * **Reconnect & retry** — idle connections are health-checked, dead ones replaced. If a connection
338
+ drops mid-*read* the read is retried (`retries=2`). Writes are never retried blindly, because the
339
+ server may already have applied them; the connection is replaced so the next call works.
340
+ * **Observability** — `echo=True`, `before_query` / `after_query` hooks (with duration and row
341
+ count), and the `pyflashdb.query` / `pyflashdb.audit` / `pyflashdb.hooks` loggers.
342
+ * **Typed** — full type hints and a `py.typed` marker, so IDE completion and mypy work.
343
+ * **`raw()`** — `flash.raw("SELECT * FROM users WHERE age > %s", [18])`. The placeholder is `%s` on
344
+ every SQL backend, SQLite included (write `%%` for a literal percent sign when passing params).
345
+ Statements that return rows give a `list[dict]` (possibly empty); others give the row count.
346
+ MongoDB: `flash.raw({"age": {"$gt": 18}}, table="users")` or `flash.raw("dbStats")`.
347
+
348
+ ---
349
+
350
+ ## Known differences
351
+
352
+ pyflashdb removes the differences that can be removed. These remain, by nature of the databases:
353
+
354
+ * **Ids**: `int` vs `str` (above). MongoDB documents contain only the fields you stored — a missing
355
+ field is not turned into `None`.
356
+ * **String collation**: tables created by `create_table` use a binary collation on MySQL, so `=`,
357
+ `IN` and `LIKE` are case-sensitive like everywhere else. On tables you created yourself, `=` follows
358
+ the column's own collation (`like` / `ilike` are correct regardless). Sort order of non-ASCII text
359
+ and non-ASCII case folding for `ilike` follow each database's locale.
360
+ * **SQLite** stores `decimal` as a numeric value (precision beyond ~15 digits is not guaranteed) and
361
+ has a single writer, so it uses one connection.
362
+ * **MongoDB**: transactions need a replica set; `bulk_insert` is not atomic outside a transaction; no
363
+ column defaults.
364
+ * **MySQL**: `upsert` fires on any unique key; `JSON` columns need MySQL ≥ 5.7.
365
+
366
+ ---
367
+
368
+ ## Upgrading from 1.x
369
+
370
+ 2.0 is a major release. What to change:
371
+
372
+ * **Import `pyflashdb`**: `from pyflashdb import FlashDB`. `import flash` still works for now but
373
+ warns and will be removed in a later release.
374
+ * **Python ≥ 3.9** (3.8 is end-of-life).
375
+ * **`before_*` hooks can now abort** (raise to cancel the operation) — in 1.x exceptions were
376
+ swallowed and printed. `after_*` hook errors are logged via `logging`, no longer `print`ed.
377
+ * **`describe()`** returns `[{"name", "type", "nullable", "default", "primary_key"}]` on every backend.
378
+ * **`!=` / `not in`** now include NULLs on SQL (as MongoDB always did); **`like`** is case-sensitive on
379
+ MySQL (it was case-insensitive); **`order_by="-x"`** now really sorts descending on SQL;
380
+ **`update()`** returns rows matched (MySQL used to return rows changed); Mongo `select(fields=…)`
381
+ no longer includes `_id`.
382
+ * **Unknown filter operators** (including raw `$gt`-style operators on Mongo) raise `FilterError`.
383
+ * **`float` columns** are now double precision (`DOUBLE` / `DOUBLE PRECISION`), and `timestamp`
384
+ columns on MySQL are `DATETIME(6)`.
385
+ * **PostgreSQL `truncate()`** no longer cascades into other tables.
386
+ * `create_index()` now works on every backend (it was MongoDB-only).
387
+
388
+ Full list: [Changelog](https://github.com/AmanDev17/pyflashdb/blob/main/Changelog.md).
389
+
390
+ ---
391
+
392
+ ## How pyflashdb compares
393
+
394
+ | | pyflashdb | SQLAlchemy | Django ORM | Raw drivers |
395
+ | ------------------------- | :-------: | :--------: | :--------: | :---------: |
396
+ | MySQL / PostgreSQL | ✅ | ✅ | ✅ | ✅ |
397
+ | SQLite | ✅ | ✅ | ✅ | ✅ |
398
+ | MongoDB | ✅ | ❌ | ❌ | ✅ |
399
+ | Same API across all DBs | ✅ | ❌ | ❌ | ❌ |
400
+ | Copy tables between DBs | ✅ | ❌ | ❌ | ❌ |
401
+
402
+ ---
403
+
404
+ ## Development
405
+
406
+ ```bash
407
+ pip install -e ".[all,dev]"
408
+ pytest # SQLite + unit tests; MySQL/PostgreSQL/Mongo are skipped unless configured
409
+ docker compose up -d --wait # start MySQL, PostgreSQL and a MongoDB replica set, then export:
410
+ export PYFLASHDB_TEST_MYSQL_URL=mysql://root:flash@127.0.0.1:3306/flash_test
411
+ export PYFLASHDB_TEST_POSTGRES_URL=postgresql://postgres:flash@127.0.0.1:5432/flash_test
412
+ export PYFLASHDB_TEST_MONGODB_URL='mongodb://127.0.0.1:27017/flash_test?replicaSet=rs0'
413
+ pytest -ra # the full parity suite
414
+ ```
415
+
416
+ ## Documentation
417
+
418
+ 📄 [Full Documentation (PDF, v2.0.0)](https://github.com/AmanDev17/pyflashdb/blob/main/pyflashdb_v_2.0.0_README.pdf) —
419
+ a 39-page manual covering every method, the cross-database behavior contract, production use, upgrading from 1.x, and
420
+ the full list of what's new in 2.0 (last chapter).
421
+
422
+ ## Links
423
+
424
+ * **PyPI:** https://pypi.org/project/pyflashdb/
425
+ * **Issues:** https://github.com/AmanDev17/pyflashdb/issues
426
+ * **Changelog:** https://github.com/AmanDev17/pyflashdb/blob/main/Changelog.md
427
+
428
+ ---
429
+
430
+ <div align="center">
431
+
432
+ **pyflashdb v2.0.0** · MIT License · Python 3.9+
433
+
434
+ *One API. Any database. Same behavior.*
435
+
436
+ </div>
437
+
438
+ ---
439
+
440
+ ## Made possible because of Komal