minidb-db 0.1.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 (44) hide show
  1. minidb_db-0.1.0/LICENSE +21 -0
  2. minidb_db-0.1.0/MANIFEST.in +11 -0
  3. minidb_db-0.1.0/PKG-INFO +407 -0
  4. minidb_db-0.1.0/README.md +374 -0
  5. minidb_db-0.1.0/app/__init__.py +1 -0
  6. minidb_db-0.1.0/app/database.py +79 -0
  7. minidb_db-0.1.0/app/demo.py +93 -0
  8. minidb_db-0.1.0/app/repositories.py +249 -0
  9. minidb_db-0.1.0/bench/__init__.py +1 -0
  10. minidb_db-0.1.0/bench/benchmark.py +127 -0
  11. minidb_db-0.1.0/minidb/__init__.py +79 -0
  12. minidb_db-0.1.0/minidb/aggregation.py +738 -0
  13. minidb_db-0.1.0/minidb/bson.py +151 -0
  14. minidb_db-0.1.0/minidb/collection.py +696 -0
  15. minidb_db-0.1.0/minidb/comparison.py +322 -0
  16. minidb_db-0.1.0/minidb/database.py +136 -0
  17. minidb_db-0.1.0/minidb/demo.py +212 -0
  18. minidb_db-0.1.0/minidb/engine.py +644 -0
  19. minidb_db-0.1.0/minidb/errors.py +68 -0
  20. minidb_db-0.1.0/minidb/index.py +389 -0
  21. minidb_db-0.1.0/minidb/objectid.py +105 -0
  22. minidb_db-0.1.0/minidb/paths.py +92 -0
  23. minidb_db-0.1.0/minidb/query.py +442 -0
  24. minidb_db-0.1.0/minidb/update.py +509 -0
  25. minidb_db-0.1.0/minidb/validation.py +246 -0
  26. minidb_db-0.1.0/minidb/webui/__init__.py +7 -0
  27. minidb_db-0.1.0/minidb/webui/__main__.py +46 -0
  28. minidb_db-0.1.0/minidb/webui/server.py +403 -0
  29. minidb_db-0.1.0/minidb/webui/static/app.css +255 -0
  30. minidb_db-0.1.0/minidb/webui/static/app.js +449 -0
  31. minidb_db-0.1.0/minidb/webui/static/index.html +145 -0
  32. minidb_db-0.1.0/minidb_db.egg-info/PKG-INFO +407 -0
  33. minidb_db-0.1.0/minidb_db.egg-info/SOURCES.txt +42 -0
  34. minidb_db-0.1.0/minidb_db.egg-info/dependency_links.txt +1 -0
  35. minidb_db-0.1.0/minidb_db.egg-info/entry_points.txt +2 -0
  36. minidb_db-0.1.0/minidb_db.egg-info/requires.txt +7 -0
  37. minidb_db-0.1.0/minidb_db.egg-info/top_level.txt +1 -0
  38. minidb_db-0.1.0/pyproject.toml +70 -0
  39. minidb_db-0.1.0/schemas/__init__.py +1 -0
  40. minidb_db-0.1.0/schemas/collections.py +114 -0
  41. minidb_db-0.1.0/setup.cfg +4 -0
  42. minidb_db-0.1.0/tests/test_minidb.py +700 -0
  43. minidb_db-0.1.0/tests/test_parity.py +427 -0
  44. minidb_db-0.1.0/tests/test_webui.py +239 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 V1PASH
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
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+
5
+ recursive-include tests *.py
6
+ recursive-include app *.py
7
+ recursive-include schemas *.py
8
+ recursive-include bench *.py
9
+
10
+ global-exclude __pycache__
11
+ global-exclude *.py[cod]
@@ -0,0 +1,407 @@
1
+ Metadata-Version: 2.4
2
+ Name: minidb-db
3
+ Version: 0.1.0
4
+ Summary: A MongoDB-style document database written from scratch, with schema validation and a built-in web UI.
5
+ Author: V1PASH
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/V1PASH/Database
8
+ Project-URL: Repository, https://github.com/V1PASH/Database
9
+ Project-URL: Issues, https://github.com/V1PASH/Database/issues
10
+ Project-URL: Documentation, https://github.com/V1PASH/Database#readme
11
+ Keywords: database,document-database,nosql,mongodb,embedded-database,sqlite,query-engine
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Database
20
+ Classifier: Topic :: Database :: Database Engines/Servers
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: pyflakes>=3.0; extra == "dev"
29
+ Requires-Dist: build>=1.0; extra == "dev"
30
+ Requires-Dist: twine>=5.0; extra == "dev"
31
+ Requires-Dist: pymongo>=4.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # minidb
35
+
36
+ [![CI](https://github.com/V1PASH/Database/actions/workflows/ci.yml/badge.svg)](https://github.com/V1PASH/Database/actions/workflows/ci.yml)
37
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](https://www.python.org/)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
39
+
40
+ **A MongoDB-style document database, written from scratch.**
41
+
42
+ You asked for a database of your own that behaves like MongoDB. This is one:
43
+ a document store with a real query engine, its own index structures, its own
44
+ aggregation pipeline and schema validation -- built on top of SQLite for
45
+ durability, but with none of MongoDB's behaviour handed to you for free.
46
+
47
+ The API is deliberately shaped like PyMongo's, so application code written
48
+ against minidb keeps working if you ever point it at a real MongoDB server.
49
+
50
+ ```python
51
+ from minidb import Database
52
+
53
+ db = Database("app.db")
54
+ db.create_collection("users", validator={"required": ["name", "email"]})
55
+
56
+ db.users.insert_one({"name": "Ada", "email": "ada@example.com", "role": "admin"})
57
+ db.users.find_one({"email": "ada@example.com"})
58
+ db.users.update_one({"name": "Ada"}, {"$inc": {"logins": 1}})
59
+ db.users.create_index("email", unique=True)
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Is it really MongoDB-compatible?
65
+
66
+ Rather than guess, minidb is tested **against a real MongoDB server**. Every
67
+ filter, update and pipeline in `tests/test_parity.py` is executed against
68
+ both databases and the results compared:
69
+
70
+ ```
71
+ $ pytest tests/ -q
72
+ 163 passed
73
+ ```
74
+
75
+ That suite is what pinned down the subtle parts of MongoDB's behaviour --
76
+ for example, that range operators *type bracket* (a `null` field never
77
+ matches `{"age": {"$lt": 36}}`), that `$min`/`$max` ignore nulls, and that
78
+ `$addToSet` ordering is deliberately unspecified.
79
+
80
+ If you have MongoDB running locally, the parity tests use it. Without it,
81
+ they skip and the rest of the suite still runs.
82
+
83
+ ---
84
+
85
+ ## Getting started
86
+
87
+ ```bash
88
+ pip install minidb-db # from PyPI
89
+ pip install -e ".[dev]" # from a clone, with the test tools
90
+ ```
91
+
92
+ Then:
93
+
94
+ ```bash
95
+ python -m app.demo # a guided tour of the example app
96
+ python -m bench.benchmark # what indexes buy you
97
+ pytest tests/ -q # the test suite
98
+ minidb-ui app.db # browse the database in your browser
99
+ ```
100
+
101
+ The import name is `minidb`, whatever the package is called:
102
+
103
+ ```python
104
+ from minidb import Database
105
+
106
+ db = Database("app.db")
107
+ db.users.insert_one({"name": "Ada", "email": "ada@example.com"})
108
+ ```
109
+
110
+ The package has **no runtime dependencies** -- it is pure Python on top of
111
+ the standard library's `sqlite3`.
112
+
113
+ ---
114
+
115
+ ## What is in the box
116
+
117
+ | Area | What works |
118
+ |---|---|
119
+ | **Documents** | JSON documents with `ObjectId`, `datetime`, `Decimal`, `UUID` and `bytes` support |
120
+ | **Queries** | `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` `$in` `$nin` `$exists` `$regex` `$options` `$all` `$size` `$elemMatch` `$not` `$type` `$mod`, plus `$and` `$or` `$nor` |
121
+ | **Paths** | Dotted paths (`"address.city"`), implicit traversal into arrays |
122
+ | **Updates** | `$set` `$unset` `$inc` `$mul` `$min` `$max` `$push` `$addToSet` `$pop` `$pull` `$pullAll` `$rename` `$setOnInsert` `$currentDate`, `arrayFilters`, upserts, replacements |
123
+ | **Aggregation** | `$match` `$project` `$addFields`/`$set` `$unset` `$group` `$sort` `$limit` `$skip` `$count` `$unwind` `$replaceRoot` `$sortByCount` and ~30 expressions |
124
+ | **Schemas** | Per-collection validators: types, `required`, `enum`, `pattern`, numeric bounds, array rules, `additionalProperties`, and **`default` values** |
125
+ | **Indexes** | Single-field and **compound** indexes (unique or not), multikey (array) indexes, order-preserving keys that serve equality *and* range lookups |
126
+ | **Writes** | Transactions with rollback, unique-constraint enforcement, `ObjectId` generation |
127
+ | **Extras** | `explain()` to see the query plan, `distinct()`, `count_documents()`, cursors with `sort`/`skip`/`limit` |
128
+
129
+ ---
130
+
131
+ ## How it works
132
+
133
+ ```
134
+ your app
135
+ |
136
+ minidb/collection.py the API (PyMongo shaped)
137
+ |
138
+ +----------+-----------+-------------------+
139
+ | | | |
140
+ query.py update.py aggregation.py validation.py
141
+ | | | |
142
+ +----------+-----------+-------------------+
143
+ |
144
+ index.py the planner + the key encoding
145
+ |
146
+ engine.py SQLite: documents, index tables, transactions
147
+ |
148
+ SQLite file
149
+ ```
150
+
151
+ **Storage.** Documents are stored as JSON text, one row per document, keyed
152
+ by `(collection, id)`. SQLite gives durability, atomicity and crash safety
153
+ for free -- but every decision that defines *how the database behaves*
154
+ lives in this project, not in SQLite.
155
+
156
+ **Indexes.** minidb keeps its own index tables (`_minidb_index_entries`),
157
+ keyed by `(collection, index, encoded_key, document_id)` and stored
158
+ `WITHOUT ROWID`, so SQLite's B-tree does the work. The encoding is
159
+ **order preserving**: a value is mapped onto bytes whose byte order matches
160
+ MongoDB's BSON comparison order, which is what lets one index serve both
161
+ equality lookups and range scans. Each component of a key is
162
+ self-delimiting, so a compound key's components are compared one after the
163
+ other.
164
+
165
+ **The planner.** `index.py` looks at the filter, finds an indexed field it
166
+ can use, and hands the engine a plan (`eq`, `prefix` or `range`). The engine
167
+ then returns a *candidate set* -- always a **superset** of the matching
168
+ documents -- and the full filter is applied in Python afterwards. That
169
+ last step is what keeps the answers correct: an index may be an
170
+ approximation, but it can never change the result.
171
+
172
+ **Type ordering.** MongoDB compares values of different types using a fixed
173
+ order (null < numbers < strings < objects < arrays < ... < dates). minidb
174
+ reproduces that in `comparison.py`, which is why `{"a": {"$gt": 5}}` behaves
175
+ the same in both.
176
+
177
+ ---
178
+
179
+ ## Using it
180
+
181
+ ### Reading
182
+
183
+ ```python
184
+ db.users.find_one({"email": "ada@example.com"})
185
+ db.users.find({"age": {"$gte": 30}}, sort=[("age", -1)], limit=10)
186
+ db.users.find({"address.city": "London"}).sort("name").skip(20)
187
+
188
+ db.users.find_one({"name": "Ada"}, {"name": 1, "email": 1, "_id": 0})
189
+ ```
190
+
191
+ ### Writing
192
+
193
+ ```python
194
+ db.users.insert_one({"name": "Ada", "email": "ada@example.com"})
195
+ db.users.insert_many([...])
196
+
197
+ db.users.update_one({"_id": oid}, {"$set": {"role": "admin"}, "$inc": {"logins": 1}})
198
+ db.users.update_many({"role": "viewer"}, {"$set": {"active": False}})
199
+ db.users.replace_one({"_id": oid}, {"name": "Ada", "email": "a@b.com"})
200
+
201
+ db.users.update_one(
202
+ {"_id": oid},
203
+ {"$set": {"items.$[i].done": True}},
204
+ array_filters=[{"i.qty": {"$gte": 2}}],
205
+ )
206
+ ```
207
+
208
+ ### Aggregating
209
+
210
+ ```python
211
+ list(db.tasks.aggregate([
212
+ {"$match": {"project_id": project_id}},
213
+ {"$group": {"_id": "$assignee_id", "hours": {"$sum": "$estimate_hours"}}},
214
+ {"$sort": {"hours": -1}},
215
+ ]))
216
+ ```
217
+
218
+ ### Schemas
219
+
220
+ ```python
221
+ db.create_collection("users", validator={
222
+ "required": ["name", "email"],
223
+ "additionalProperties": False,
224
+ "properties": {
225
+ "name": {"type": "string", "minLength": 1},
226
+ "email": {"type": "string", "format": "email"},
227
+ "role": {"type": "string", "enum": ["admin", "member"], "default": "member"},
228
+ },
229
+ })
230
+ ```
231
+
232
+ A document that breaks the schema is rejected on write, with a message
233
+ pointing at the offending field. `default` values are filled in for you.
234
+
235
+ ### Indexes
236
+
237
+ ```python
238
+ db.users.create_index("email", unique=True)
239
+ db.tasks.create_index([("project_id", 1), ("status", 1)]) # compound
240
+ db.users.list_indexes()
241
+ db.users.explain({"email": "ada@example.com"})
242
+ ```
243
+
244
+ ```
245
+ {'strategy': 'IXSCAN', 'index': 'email_1', 'documentsExamined': 1, 'totalDocuments': 20000}
246
+ ```
247
+
248
+ A **compound index** covers several fields. Its key is the concatenation of
249
+ self-delimiting component keys, so the components are compared one after
250
+ the other -- exactly like MongoDB, whose compound keys are compared
251
+ component by component. That is what lets one index answer:
252
+
253
+ | Query | What the index does |
254
+ |---|---|
255
+ | `{"a": 1, "b": 2}` | exact key lookup |
256
+ | `{"a": 1}` | prefix scan |
257
+ | `{"a": 1, "b": {"$gt": 5}}` | one contiguous range of keys |
258
+ | `{"b": 2}` | not usable -- `a` comes first, so the index cannot help |
259
+
260
+ On 20,000 documents, a compound index turned a 55.9 ms scan into a
261
+ 0.01 ms lookup.
262
+
263
+ ---
264
+
265
+ ## The web UI
266
+
267
+ Every database deserves a way to look at it. minidb ships with one:
268
+
269
+ ```bash
270
+ minidb-ui app.db # or: python -m minidb.webui app.db
271
+ ```
272
+
273
+ No data yet? Seed a sample database first:
274
+
275
+ ```bash
276
+ python -c "from minidb import Database; from minidb.demo import seed; seed(Database('demo.db'))"
277
+ minidb-ui demo.db
278
+ ```
279
+
280
+ <img src="docs/ui.png" alt="The minidb web UI" width="100%">
281
+
282
+ It opens on `http://127.0.0.1:8080` and gives you:
283
+
284
+ - **Browse** -- every collection with its document count, its indexes, and a
285
+ filter/sort/limit toolbar
286
+ - **Query** -- a filter, a projection, a sort, a limit and a skip
287
+ - **Aggregate** -- a whole aggregation pipeline
288
+ - **Insert** -- a document, validated by the collection's schema
289
+ - **Document detail** -- click any document for highlighted JSON, and delete
290
+ it from there
291
+
292
+ The server is built on `http.server` from the standard library, so it has no
293
+ dependencies of its own. It binds to localhost and has **no authentication**:
294
+ it is a local development tool, not something to expose to a network.
295
+
296
+ ---
297
+
298
+ ## The example application
299
+
300
+ `app/` is a small but complete backend built on the database:
301
+
302
+ | File | What it holds |
303
+ |---|---|
304
+ | `schemas/collections.py` | The schemas for `users`, `projects`, `tasks`, `notes` |
305
+ | `app/database.py` | Opening the database, creating collections and indexes |
306
+ | `app/repositories.py` | The access layer the application actually uses |
307
+ | `app/demo.py` | A runnable tour (`python -m app.demo`) |
308
+
309
+ Keeping every database call inside the repositories means the storage
310
+ engine can be swapped later without touching the rest of the app -- swap
311
+ `minidb.Database` for `pymongo.MongoClient` and the repositories keep
312
+ working.
313
+
314
+ ---
315
+
316
+ ## Differences from MongoDB (the honest list)
317
+
318
+ minidb is a from-scratch implementation, so some things are missing or
319
+ behave differently. Everything here is a deliberate limitation, not a bug:
320
+
321
+ **Not implemented**
322
+
323
+ - Descending index directions -- compound indexes are ascending only, and
324
+ `create_index` says so rather than quietly ignoring the direction.
325
+ - `$lookup`, `$facet`, `$out`, `$merge`, `$graphLookup`, `$unionWith`.
326
+ - The positional `$` operator in updates (`"items.$.qty"`). `arrayFilters`
327
+ (`"items.$[i].qty"`) do work.
328
+ - Change streams, geospatial queries, full-text search, map/reduce.
329
+ - A network protocol: minidb is an embedded library, not a server.
330
+ - Replication, sharding, and transactions spanning collections.
331
+
332
+ **Different on purpose**
333
+
334
+ - `find()` returns documents in insertion order, and sorting ties keep
335
+ insertion order. MongoDB does not promise an order for ties.
336
+ - `$addToSet` keeps insertion order; MongoDB's order is unspecified.
337
+ - `additionalProperties: False` exempts `_id`, which minidb adds itself.
338
+ - The `_id` index is always present; there is no way to drop it.
339
+ - A document with several very large array fields in one compound index
340
+ needs too many key combinations to index completely. minidb keeps the
341
+ document findable through an overflow table rather than hiding it.
342
+
343
+ ---
344
+
345
+ ## Continuous integration
346
+
347
+ Every push runs the suite on Python 3.11, 3.12 and 3.13 -- against a real
348
+ MongoDB service, so the parity tests actually run rather than skipping
349
+ themselves. The browser code is checked under node, and the package is
350
+ built, checked with `twine` and installed into a clean environment.
351
+
352
+ Releasing to PyPI is one tag away, once the repository is registered as a
353
+ trusted publisher on PyPI:
354
+
355
+ ```bash
356
+ git tag v0.1.0 && git push origin v0.1.0
357
+ ```
358
+
359
+ ---
360
+
361
+ ## Where to take it next
362
+
363
+ The interesting problems in a database are all still open here:
364
+
365
+ 1. **Descending index directions** -- a compound key whose components can
366
+ each be scanned backwards, and a planner that uses them for reverse sorts.
367
+ 2. **A real storage engine** -- replace the SQLite file with a write-ahead
368
+ log, free-space reuse and page-level compaction.
369
+ 3. **Concurrency** -- MVCC with snapshot isolation instead of a single
370
+ write lock.
371
+ 4. **A server** -- a wire protocol and a client library, so minidb can be
372
+ used from another language.
373
+ 5. **A better planner** -- cost estimates, index intersection, and choosing
374
+ between several usable indexes.
375
+ 6. **Full-text search** -- an inverted index over tokenised documents.
376
+
377
+ ---
378
+
379
+ ## Layout
380
+
381
+ ```
382
+ minidb/ the database
383
+ collection.py the API
384
+ query.py filter semantics
385
+ update.py update operators
386
+ aggregation.py the pipeline
387
+ validation.py schemas
388
+ index.py key encoding + planner
389
+ engine.py SQLite storage layer
390
+ comparison.py BSON value ordering
391
+ bson.py document codec
392
+ objectid.py ObjectId
393
+ database.py the Database object
394
+ demo.py sample data
395
+ webui/ the web UI (stdlib only)
396
+ server.py the HTTP API
397
+ static/ the browser app
398
+ schemas/ the example app's schemas
399
+ app/ the example app
400
+ bench/ benchmarks
401
+ docs/ the screenshot
402
+ tests/
403
+ test_minidb.py unit tests
404
+ test_parity.py minidb vs. real MongoDB
405
+ test_webui.py the web UI's API
406
+ frontend_smoke.mjs the browser code, in a DOM shim
407
+ ```