debugorm 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 (35) hide show
  1. debugorm-0.1.0/LICENSE +21 -0
  2. debugorm-0.1.0/PKG-INFO +308 -0
  3. debugorm-0.1.0/README.md +278 -0
  4. debugorm-0.1.0/debugorm/__init__.py +105 -0
  5. debugorm-0.1.0/debugorm/core/__init__.py +17 -0
  6. debugorm-0.1.0/debugorm/core/compiler.py +144 -0
  7. debugorm-0.1.0/debugorm/core/manager.py +207 -0
  8. debugorm-0.1.0/debugorm/core/model.py +173 -0
  9. debugorm-0.1.0/debugorm/core/pipeline.py +132 -0
  10. debugorm-0.1.0/debugorm/core/query.py +149 -0
  11. debugorm-0.1.0/debugorm/db/__init__.py +3 -0
  12. debugorm-0.1.0/debugorm/db/connection.py +84 -0
  13. debugorm-0.1.0/debugorm/fields/__init__.py +5 -0
  14. debugorm-0.1.0/debugorm/fields/base.py +37 -0
  15. debugorm-0.1.0/debugorm/fields/integer.py +24 -0
  16. debugorm-0.1.0/debugorm/fields/string.py +32 -0
  17. debugorm-0.1.0/debugorm/plugins/__init__.py +15 -0
  18. debugorm-0.1.0/debugorm/plugins/base.py +50 -0
  19. debugorm-0.1.0/debugorm/plugins/dry_run.py +68 -0
  20. debugorm-0.1.0/debugorm/plugins/explain.py +56 -0
  21. debugorm-0.1.0/debugorm/plugins/logging_plugin.py +55 -0
  22. debugorm-0.1.0/debugorm/plugins/pretty_print.py +72 -0
  23. debugorm-0.1.0/debugorm/plugins/visualize.py +80 -0
  24. debugorm-0.1.0/debugorm.egg-info/PKG-INFO +308 -0
  25. debugorm-0.1.0/debugorm.egg-info/SOURCES.txt +33 -0
  26. debugorm-0.1.0/debugorm.egg-info/dependency_links.txt +1 -0
  27. debugorm-0.1.0/debugorm.egg-info/requires.txt +4 -0
  28. debugorm-0.1.0/debugorm.egg-info/top_level.txt +1 -0
  29. debugorm-0.1.0/pyproject.toml +58 -0
  30. debugorm-0.1.0/setup.cfg +4 -0
  31. debugorm-0.1.0/tests/test_compiler.py +127 -0
  32. debugorm-0.1.0/tests/test_fields.py +65 -0
  33. debugorm-0.1.0/tests/test_model.py +147 -0
  34. debugorm-0.1.0/tests/test_plugins.py +102 -0
  35. debugorm-0.1.0/tests/test_query.py +106 -0
debugorm-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DebugORM Contributors
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,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: debugorm
3
+ Version: 0.1.0
4
+ Summary: A minimal, plugin-driven ORM for understanding and debugging SQL
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/dgolov/debugorm
7
+ Project-URL: Repository, https://github.com/dgolov/debugorm
8
+ Project-URL: Issues, https://github.com/dgolov/debugorm/issues
9
+ Project-URL: Changelog, https://github.com/dgolov/debugorm/blob/main/CHANGELOG.md
10
+ Keywords: orm,sqlite,debug,sql,database
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Debuggers
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # DebugORM
32
+
33
+ A minimal, plugin-driven ORM for SQLite whose primary goal is **helping developers understand and debug SQL queries**, not just execute them.
34
+
35
+ ---
36
+
37
+ ## Architecture
38
+
39
+ ```
40
+ Model → QuerySet → Query → Pipeline → [Plugins] → SQLCompiler → Executor → Result
41
+ ```
42
+
43
+ ### Key objects
44
+
45
+ | Object | Responsibility |
46
+ |---|---|
47
+ | `Field` | Declares a column; validates and converts values |
48
+ | `Model` | Base class; provides `save()`, `delete()`, `create_table()` |
49
+ | `Manager` | Attached as `Model.objects`; creates QuerySets |
50
+ | `QuerySet` | Chainable, lazy query builder |
51
+ | `Query` | **Structured, SQL-free** representation of a query (the internal AST) |
52
+ | `SQLCompiler` | Translates `Query` → `(sql, params)` |
53
+ | `QueryPipeline` | Orchestrates the full lifecycle and calls plugin hooks |
54
+ | `Plugin` | Base class for all debug extensions |
55
+
56
+ ### Plugin hook order
57
+
58
+ ```
59
+ before_compile(query, ctx)
60
+
61
+ SQLCompiler.compile(query) → CompiledQuery
62
+
63
+ after_compile(compiled, ctx)
64
+
65
+ before_execute(compiled, ctx) ← return QueryResult here to skip execution
66
+
67
+ QueryExecutor.execute(compiled) → QueryResult
68
+
69
+ after_execute(result, ctx)
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Quick start
75
+
76
+ ```python
77
+ from debugorm import configure, Model
78
+ from debugorm.fields import IntegerField, StringField
79
+
80
+ configure(":memory:") # or a file path: "myapp.db"
81
+
82
+ class User(Model):
83
+ id = IntegerField(primary_key=True)
84
+ name = StringField(max_length=100)
85
+ age = IntegerField()
86
+
87
+ User.create_table()
88
+ ```
89
+
90
+ ### CRUD
91
+
92
+ ```python
93
+ # Create
94
+ alice = User(name="Alice", age=25)
95
+ alice.save() # INSERT
96
+
97
+ alice.age = 26
98
+ alice.save() # UPDATE
99
+
100
+ alice.delete() # DELETE
101
+
102
+ # Read
103
+ User.objects.all()
104
+ User.objects.filter(age__gte=18).order_by("-age").all()
105
+ User.objects.get(name="Bob")
106
+ User.objects.filter(age__gt=20).count()
107
+ User.objects.order_by("-age").first()
108
+ ```
109
+
110
+ ### Supported lookup operators
111
+
112
+ | Suffix | SQL |
113
+ |---|---|
114
+ | `age__eq=18` or `age=18` | `age = 18` |
115
+ | `age__ne=18` | `age != 18` |
116
+ | `age__gt=18` | `age > 18` |
117
+ | `age__gte=18` | `age >= 18` |
118
+ | `age__lt=18` | `age < 18` |
119
+ | `age__lte=18` | `age <= 18` |
120
+ | `name__like="A%"` | `name LIKE 'A%'` |
121
+ | `name__in=["Alice","Bob"]` | `name IN ('Alice', 'Bob')` |
122
+ | `name__isnull=True` | `name IS NULL` |
123
+
124
+ ---
125
+
126
+ ## Debug plugins
127
+
128
+ Attach plugins on the fly with `.debug()`:
129
+
130
+ ```python
131
+ User.objects.debug(
132
+ explain=True, # EXPLAIN QUERY PLAN
133
+ dry_run=True, # show SQL without executing
134
+ log=True, # log SQL + timing via Python logging
135
+ visualize=True, # print query tree
136
+ pretty=True, # pretty-format SQL
137
+ ).filter(age__gt=18).all()
138
+ ```
139
+
140
+ ### ExplainPlugin
141
+
142
+ Runs `EXPLAIN QUERY PLAN` after query execution and prints how SQLite plans to process it.
143
+
144
+ ```
145
+ ╔────────────────────────────────────────────────────────────╗
146
+ ║ [ExplainPlugin]
147
+ ╠────────────────────────────────────────────────────────────╣
148
+ ║ SQL : SELECT * FROM users WHERE age > ? ORDER BY age DESC
149
+ ╠────────────────────────────────────────────────────────────╣
150
+ ║ QUERY PLAN
151
+ ║ └── SCAN users
152
+ ║ └── USE TEMP B-TREE FOR ORDER BY
153
+ ╚────────────────────────────────────────────────────────────╝
154
+ ```
155
+
156
+ ### DryRunPlugin
157
+
158
+ Prevents execution; estimates how many rows would be affected.
159
+
160
+ ```
161
+ ╔────────────────────────────────────────────────────────────╗
162
+ ║ [DryRunPlugin] ⚠ NOT EXECUTED
163
+ ╠────────────────────────────────────────────────────────────╣
164
+ ║ SQL : SELECT * FROM users WHERE age >= ? ORDER BY name ASC
165
+ ║ Interpolated: SELECT * FROM users WHERE age >= 18 ORDER BY name ASC
166
+ ╠────────────────────────────────────────────────────────────╣
167
+ ║ Estimated rows affected: ~3
168
+ ╚────────────────────────────────────────────────────────────╝
169
+ ```
170
+
171
+ ### LoggingPlugin
172
+
173
+ Uses Python's `logging` module — integrates with any existing log config.
174
+
175
+ ```
176
+ DEBUG debugorm.sql › SQL ▶ SELECT * FROM users WHERE age > ? params=(20,)
177
+ DEBUG debugorm.sql › SQL ◀ rows=3 time=0.12ms
178
+ ```
179
+
180
+ ### VisualizePlugin
181
+
182
+ Renders the query as a tree **before** compilation (shows intent, not SQL).
183
+
184
+ ```
185
+ ╔────────────────────────────────────────────────────────────╗
186
+ ║ [VisualizePlugin] Query Structure
187
+ ╠────────────────────────────────────────────────────────────╣
188
+ ║ User (SELECT)
189
+ ║ ├── filter: age > 18
190
+ ║ ├── filter: name LIKE 'A%'
191
+ ║ ├── order_by: name ASC
192
+ ║ └── limit: 10
193
+ ╚────────────────────────────────────────────────────────────╝
194
+ ```
195
+
196
+ Pass `show_json=True` to also get the JSON representation.
197
+
198
+ ### PrettyPrintPlugin
199
+
200
+ Formats the compiled SQL for human readability.
201
+
202
+ ```
203
+ SELECT *
204
+ FROM users
205
+ WHERE age >= ?
206
+ ORDER BY age DESC
207
+ LIMIT ?
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Query Diff
213
+
214
+ Compare two queries at the **internal representation level** (not string comparison):
215
+
216
+ ```python
217
+ q1 = User.objects.filter(age__gt=18)
218
+ q2 = User.objects.filter(age__gt=21)
219
+
220
+ print(q1.diff(q2))
221
+ # - age > 18
222
+ # + age > 21
223
+
224
+ q3 = User.objects.filter(age__gte=18).order_by("-age").limit(10)
225
+ q4 = User.objects.filter(age__gte=18).order_by("name").limit(5)
226
+
227
+ print(q3.diff(q4))
228
+ # - ORDER BY age DESC
229
+ # + ORDER BY name ASC
230
+ # - LIMIT 10
231
+ # + LIMIT 5
232
+
233
+ print(q1.diff(q1))
234
+ # Queries are identical
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Global plugin configuration via DebugORM facade
240
+
241
+ ```python
242
+ from debugorm import DebugORM, LoggingPlugin, PrettyPrintPlugin
243
+
244
+ orm = DebugORM(":memory:", plugins=[LoggingPlugin(), PrettyPrintPlugin()])
245
+ orm.install() # applies the pipeline to every already-defined model
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Project structure
251
+
252
+ ```
253
+ debugorm/
254
+ ├── __init__.py public API
255
+ ├── core/
256
+ │ ├── model.py Model base class + ModelMeta
257
+ │ ├── query.py Query AST + FilterCondition + Query.diff()
258
+ │ ├── compiler.py SQLCompiler → CompiledQuery
259
+ │ ├── pipeline.py QueryPipeline + QueryExecutor + QueryResult
260
+ │ └── manager.py Manager + QuerySet
261
+ ├── fields/
262
+ │ ├── base.py
263
+ │ ├── integer.py
264
+ │ └── string.py
265
+ ├── plugins/
266
+ │ ├── base.py Plugin base class + hook signatures
267
+ │ ├── explain.py ExplainPlugin
268
+ │ ├── dry_run.py DryRunPlugin
269
+ │ ├── logging_plugin.py LoggingPlugin
270
+ │ ├── visualize.py VisualizePlugin
271
+ │ └── pretty_print.py PrettyPrintPlugin
272
+ └── db/
273
+ └── connection.py Connection + configure()
274
+ demo.py end-to-end usage example
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Writing a custom plugin
280
+
281
+ ```python
282
+ from debugorm.plugins.base import Plugin
283
+
284
+ class TimingPlugin(Plugin):
285
+ name = "timing"
286
+
287
+ def before_execute(self, compiled, ctx):
288
+ import time
289
+ ctx.extra["t0"] = time.perf_counter()
290
+ return None # don't intercept
291
+
292
+ def after_execute(self, result, ctx):
293
+ elapsed = (time.perf_counter() - ctx.extra["t0"]) * 1000
294
+ print(f"[TimingPlugin] {elapsed:.1f}ms — {result.rowcount} rows")
295
+
296
+
297
+ # Use it:
298
+ User.objects.debug().filter(age__gt=18) # ... or
299
+ from debugorm import QueryPipeline
300
+ User.objects.pipeline = QueryPipeline(plugins=[TimingPlugin()])
301
+ ```
302
+
303
+ ---
304
+
305
+ ## Requirements
306
+
307
+ - Python 3.10+
308
+ - No external dependencies (standard library only: `sqlite3`, `logging`, `dataclasses`, `copy`, `re`, `time`, `json`)
@@ -0,0 +1,278 @@
1
+ # DebugORM
2
+
3
+ A minimal, plugin-driven ORM for SQLite whose primary goal is **helping developers understand and debug SQL queries**, not just execute them.
4
+
5
+ ---
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ Model → QuerySet → Query → Pipeline → [Plugins] → SQLCompiler → Executor → Result
11
+ ```
12
+
13
+ ### Key objects
14
+
15
+ | Object | Responsibility |
16
+ |---|---|
17
+ | `Field` | Declares a column; validates and converts values |
18
+ | `Model` | Base class; provides `save()`, `delete()`, `create_table()` |
19
+ | `Manager` | Attached as `Model.objects`; creates QuerySets |
20
+ | `QuerySet` | Chainable, lazy query builder |
21
+ | `Query` | **Structured, SQL-free** representation of a query (the internal AST) |
22
+ | `SQLCompiler` | Translates `Query` → `(sql, params)` |
23
+ | `QueryPipeline` | Orchestrates the full lifecycle and calls plugin hooks |
24
+ | `Plugin` | Base class for all debug extensions |
25
+
26
+ ### Plugin hook order
27
+
28
+ ```
29
+ before_compile(query, ctx)
30
+
31
+ SQLCompiler.compile(query) → CompiledQuery
32
+
33
+ after_compile(compiled, ctx)
34
+
35
+ before_execute(compiled, ctx) ← return QueryResult here to skip execution
36
+
37
+ QueryExecutor.execute(compiled) → QueryResult
38
+
39
+ after_execute(result, ctx)
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ from debugorm import configure, Model
48
+ from debugorm.fields import IntegerField, StringField
49
+
50
+ configure(":memory:") # or a file path: "myapp.db"
51
+
52
+ class User(Model):
53
+ id = IntegerField(primary_key=True)
54
+ name = StringField(max_length=100)
55
+ age = IntegerField()
56
+
57
+ User.create_table()
58
+ ```
59
+
60
+ ### CRUD
61
+
62
+ ```python
63
+ # Create
64
+ alice = User(name="Alice", age=25)
65
+ alice.save() # INSERT
66
+
67
+ alice.age = 26
68
+ alice.save() # UPDATE
69
+
70
+ alice.delete() # DELETE
71
+
72
+ # Read
73
+ User.objects.all()
74
+ User.objects.filter(age__gte=18).order_by("-age").all()
75
+ User.objects.get(name="Bob")
76
+ User.objects.filter(age__gt=20).count()
77
+ User.objects.order_by("-age").first()
78
+ ```
79
+
80
+ ### Supported lookup operators
81
+
82
+ | Suffix | SQL |
83
+ |---|---|
84
+ | `age__eq=18` or `age=18` | `age = 18` |
85
+ | `age__ne=18` | `age != 18` |
86
+ | `age__gt=18` | `age > 18` |
87
+ | `age__gte=18` | `age >= 18` |
88
+ | `age__lt=18` | `age < 18` |
89
+ | `age__lte=18` | `age <= 18` |
90
+ | `name__like="A%"` | `name LIKE 'A%'` |
91
+ | `name__in=["Alice","Bob"]` | `name IN ('Alice', 'Bob')` |
92
+ | `name__isnull=True` | `name IS NULL` |
93
+
94
+ ---
95
+
96
+ ## Debug plugins
97
+
98
+ Attach plugins on the fly with `.debug()`:
99
+
100
+ ```python
101
+ User.objects.debug(
102
+ explain=True, # EXPLAIN QUERY PLAN
103
+ dry_run=True, # show SQL without executing
104
+ log=True, # log SQL + timing via Python logging
105
+ visualize=True, # print query tree
106
+ pretty=True, # pretty-format SQL
107
+ ).filter(age__gt=18).all()
108
+ ```
109
+
110
+ ### ExplainPlugin
111
+
112
+ Runs `EXPLAIN QUERY PLAN` after query execution and prints how SQLite plans to process it.
113
+
114
+ ```
115
+ ╔────────────────────────────────────────────────────────────╗
116
+ ║ [ExplainPlugin]
117
+ ╠────────────────────────────────────────────────────────────╣
118
+ ║ SQL : SELECT * FROM users WHERE age > ? ORDER BY age DESC
119
+ ╠────────────────────────────────────────────────────────────╣
120
+ ║ QUERY PLAN
121
+ ║ └── SCAN users
122
+ ║ └── USE TEMP B-TREE FOR ORDER BY
123
+ ╚────────────────────────────────────────────────────────────╝
124
+ ```
125
+
126
+ ### DryRunPlugin
127
+
128
+ Prevents execution; estimates how many rows would be affected.
129
+
130
+ ```
131
+ ╔────────────────────────────────────────────────────────────╗
132
+ ║ [DryRunPlugin] ⚠ NOT EXECUTED
133
+ ╠────────────────────────────────────────────────────────────╣
134
+ ║ SQL : SELECT * FROM users WHERE age >= ? ORDER BY name ASC
135
+ ║ Interpolated: SELECT * FROM users WHERE age >= 18 ORDER BY name ASC
136
+ ╠────────────────────────────────────────────────────────────╣
137
+ ║ Estimated rows affected: ~3
138
+ ╚────────────────────────────────────────────────────────────╝
139
+ ```
140
+
141
+ ### LoggingPlugin
142
+
143
+ Uses Python's `logging` module — integrates with any existing log config.
144
+
145
+ ```
146
+ DEBUG debugorm.sql › SQL ▶ SELECT * FROM users WHERE age > ? params=(20,)
147
+ DEBUG debugorm.sql › SQL ◀ rows=3 time=0.12ms
148
+ ```
149
+
150
+ ### VisualizePlugin
151
+
152
+ Renders the query as a tree **before** compilation (shows intent, not SQL).
153
+
154
+ ```
155
+ ╔────────────────────────────────────────────────────────────╗
156
+ ║ [VisualizePlugin] Query Structure
157
+ ╠────────────────────────────────────────────────────────────╣
158
+ ║ User (SELECT)
159
+ ║ ├── filter: age > 18
160
+ ║ ├── filter: name LIKE 'A%'
161
+ ║ ├── order_by: name ASC
162
+ ║ └── limit: 10
163
+ ╚────────────────────────────────────────────────────────────╝
164
+ ```
165
+
166
+ Pass `show_json=True` to also get the JSON representation.
167
+
168
+ ### PrettyPrintPlugin
169
+
170
+ Formats the compiled SQL for human readability.
171
+
172
+ ```
173
+ SELECT *
174
+ FROM users
175
+ WHERE age >= ?
176
+ ORDER BY age DESC
177
+ LIMIT ?
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Query Diff
183
+
184
+ Compare two queries at the **internal representation level** (not string comparison):
185
+
186
+ ```python
187
+ q1 = User.objects.filter(age__gt=18)
188
+ q2 = User.objects.filter(age__gt=21)
189
+
190
+ print(q1.diff(q2))
191
+ # - age > 18
192
+ # + age > 21
193
+
194
+ q3 = User.objects.filter(age__gte=18).order_by("-age").limit(10)
195
+ q4 = User.objects.filter(age__gte=18).order_by("name").limit(5)
196
+
197
+ print(q3.diff(q4))
198
+ # - ORDER BY age DESC
199
+ # + ORDER BY name ASC
200
+ # - LIMIT 10
201
+ # + LIMIT 5
202
+
203
+ print(q1.diff(q1))
204
+ # Queries are identical
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Global plugin configuration via DebugORM facade
210
+
211
+ ```python
212
+ from debugorm import DebugORM, LoggingPlugin, PrettyPrintPlugin
213
+
214
+ orm = DebugORM(":memory:", plugins=[LoggingPlugin(), PrettyPrintPlugin()])
215
+ orm.install() # applies the pipeline to every already-defined model
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Project structure
221
+
222
+ ```
223
+ debugorm/
224
+ ├── __init__.py public API
225
+ ├── core/
226
+ │ ├── model.py Model base class + ModelMeta
227
+ │ ├── query.py Query AST + FilterCondition + Query.diff()
228
+ │ ├── compiler.py SQLCompiler → CompiledQuery
229
+ │ ├── pipeline.py QueryPipeline + QueryExecutor + QueryResult
230
+ │ └── manager.py Manager + QuerySet
231
+ ├── fields/
232
+ │ ├── base.py
233
+ │ ├── integer.py
234
+ │ └── string.py
235
+ ├── plugins/
236
+ │ ├── base.py Plugin base class + hook signatures
237
+ │ ├── explain.py ExplainPlugin
238
+ │ ├── dry_run.py DryRunPlugin
239
+ │ ├── logging_plugin.py LoggingPlugin
240
+ │ ├── visualize.py VisualizePlugin
241
+ │ └── pretty_print.py PrettyPrintPlugin
242
+ └── db/
243
+ └── connection.py Connection + configure()
244
+ demo.py end-to-end usage example
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Writing a custom plugin
250
+
251
+ ```python
252
+ from debugorm.plugins.base import Plugin
253
+
254
+ class TimingPlugin(Plugin):
255
+ name = "timing"
256
+
257
+ def before_execute(self, compiled, ctx):
258
+ import time
259
+ ctx.extra["t0"] = time.perf_counter()
260
+ return None # don't intercept
261
+
262
+ def after_execute(self, result, ctx):
263
+ elapsed = (time.perf_counter() - ctx.extra["t0"]) * 1000
264
+ print(f"[TimingPlugin] {elapsed:.1f}ms — {result.rowcount} rows")
265
+
266
+
267
+ # Use it:
268
+ User.objects.debug().filter(age__gt=18) # ... or
269
+ from debugorm import QueryPipeline
270
+ User.objects.pipeline = QueryPipeline(plugins=[TimingPlugin()])
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Requirements
276
+
277
+ - Python 3.10+
278
+ - No external dependencies (standard library only: `sqlite3`, `logging`, `dataclasses`, `copy`, `re`, `time`, `json`)
@@ -0,0 +1,105 @@
1
+ """
2
+ DebugORM — a minimal, plugin-driven ORM for understanding and debugging SQL.
3
+
4
+ Quick start::
5
+
6
+ from debugorm import configure, Model
7
+ from debugorm.fields import IntegerField, StringField
8
+
9
+ configure("myapp.db") # or ":memory:"
10
+
11
+ class User(Model):
12
+ id = IntegerField(primary_key=True)
13
+ name = StringField()
14
+ age = IntegerField()
15
+
16
+ User.create_table()
17
+
18
+ alice = User(name="Alice", age=25)
19
+ alice.save()
20
+
21
+ users = User.objects.filter(age__gt=18).order_by("-age").all()
22
+
23
+ # Debug any query on the fly
24
+ User.objects.debug(explain=True, pretty=True).filter(age__gt=18).all()
25
+
26
+ # Compare two queries
27
+ q1 = User.objects.filter(age__gt=18)
28
+ q2 = User.objects.filter(age__gt=21)
29
+ print(q1.diff(q2))
30
+ """
31
+
32
+ from .core.model import Model
33
+ from .core.query import Query
34
+ from .core.pipeline import QueryPipeline, QueryResult, PipelineContext
35
+ from .core.compiler import SQLCompiler, CompiledQuery
36
+ from .core.manager import Manager, QuerySet
37
+ from .db.connection import configure, get_connection, set_connection, Connection
38
+ from .fields import Field, IntegerField, StringField
39
+ from .plugins import (
40
+ Plugin,
41
+ ExplainPlugin,
42
+ DryRunPlugin,
43
+ LoggingPlugin,
44
+ VisualizePlugin,
45
+ PrettyPrintPlugin,
46
+ )
47
+
48
+ __version__ = "0.1.0"
49
+ __all__ = [
50
+ "Model",
51
+ "Query",
52
+ "QueryPipeline",
53
+ "QueryResult",
54
+ "PipelineContext",
55
+ "SQLCompiler",
56
+ "CompiledQuery",
57
+ "Manager",
58
+ "QuerySet",
59
+ "configure",
60
+ "get_connection",
61
+ "set_connection",
62
+ "Connection",
63
+ "Field",
64
+ "IntegerField",
65
+ "StringField",
66
+ "Plugin",
67
+ "ExplainPlugin",
68
+ "DryRunPlugin",
69
+ "LoggingPlugin",
70
+ "VisualizePlugin",
71
+ "PrettyPrintPlugin",
72
+ "DebugORM",
73
+ ]
74
+
75
+
76
+ class DebugORM:
77
+ """
78
+ Optional facade for configuring a fixed plugin set across all models::
79
+
80
+ orm = DebugORM(":memory:", plugins=[ExplainPlugin(), LoggingPlugin()])
81
+ orm.install() # applies the pipeline to every existing Model subclass
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ db_path: str = ":memory:",
87
+ plugins: list | None = None,
88
+ ) -> None:
89
+ self.connection = configure(db_path)
90
+ self.plugins: list = plugins or []
91
+ self._pipeline = QueryPipeline(plugins=self.plugins, connection=self.connection)
92
+
93
+ def install(self) -> None:
94
+ """Push the configured pipeline onto every ``Model`` subclass defined so far."""
95
+ for subclass in self._all_subclasses(Model):
96
+ if hasattr(subclass, "objects"):
97
+ subclass.objects.pipeline = self._pipeline
98
+
99
+ @staticmethod
100
+ def _all_subclasses(base: type) -> list:
101
+ result = []
102
+ for cls in base.__subclasses__():
103
+ result.append(cls)
104
+ result.extend(DebugORM._all_subclasses(cls))
105
+ return result