debugorm 0.1.0__py3-none-any.whl
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.
- debugorm/__init__.py +105 -0
- debugorm/core/__init__.py +17 -0
- debugorm/core/compiler.py +144 -0
- debugorm/core/manager.py +207 -0
- debugorm/core/model.py +173 -0
- debugorm/core/pipeline.py +132 -0
- debugorm/core/query.py +149 -0
- debugorm/db/__init__.py +3 -0
- debugorm/db/connection.py +84 -0
- debugorm/fields/__init__.py +5 -0
- debugorm/fields/base.py +37 -0
- debugorm/fields/integer.py +24 -0
- debugorm/fields/string.py +32 -0
- debugorm/plugins/__init__.py +15 -0
- debugorm/plugins/base.py +50 -0
- debugorm/plugins/dry_run.py +68 -0
- debugorm/plugins/explain.py +56 -0
- debugorm/plugins/logging_plugin.py +55 -0
- debugorm/plugins/pretty_print.py +72 -0
- debugorm/plugins/visualize.py +80 -0
- debugorm-0.1.0.dist-info/METADATA +308 -0
- debugorm-0.1.0.dist-info/RECORD +25 -0
- debugorm-0.1.0.dist-info/WHEEL +5 -0
- debugorm-0.1.0.dist-info/licenses/LICENSE +21 -0
- debugorm-0.1.0.dist-info/top_level.txt +1 -0
debugorm/__init__.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .model import Model
|
|
2
|
+
from .query import Query
|
|
3
|
+
from .compiler import SQLCompiler, CompiledQuery
|
|
4
|
+
from .pipeline import QueryPipeline, QueryResult, PipelineContext
|
|
5
|
+
from .manager import Manager, QuerySet
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Model",
|
|
9
|
+
"Query",
|
|
10
|
+
"SQLCompiler",
|
|
11
|
+
"CompiledQuery",
|
|
12
|
+
"QueryPipeline",
|
|
13
|
+
"QueryResult",
|
|
14
|
+
"PipelineContext",
|
|
15
|
+
"Manager",
|
|
16
|
+
"QuerySet",
|
|
17
|
+
]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, List, Tuple
|
|
4
|
+
|
|
5
|
+
from .query import Query
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class CompiledQuery:
|
|
10
|
+
"""The output of SQL compilation: a statement string and its parameter tuple."""
|
|
11
|
+
|
|
12
|
+
sql: str
|
|
13
|
+
params: Tuple[Any, ...]
|
|
14
|
+
query: Query
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return self.sql
|
|
18
|
+
|
|
19
|
+
def interpolated(self) -> str:
|
|
20
|
+
"""SQL with parameters substituted inline — for display only."""
|
|
21
|
+
result = self.sql
|
|
22
|
+
for p in self.params:
|
|
23
|
+
placeholder = f"'{p}'" if isinstance(p, str) else str(p)
|
|
24
|
+
result = result.replace("?", placeholder, 1)
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SQLCompiler:
|
|
29
|
+
"""Translates a ``Query`` object into a ``CompiledQuery``."""
|
|
30
|
+
|
|
31
|
+
def compile(self, query: Query) -> CompiledQuery:
|
|
32
|
+
handlers = {
|
|
33
|
+
"SELECT": self._compile_select,
|
|
34
|
+
"INSERT": self._compile_insert,
|
|
35
|
+
"UPDATE": self._compile_update,
|
|
36
|
+
"DELETE": self._compile_delete,
|
|
37
|
+
}
|
|
38
|
+
handler = handlers.get(query.query_type)
|
|
39
|
+
if handler is None:
|
|
40
|
+
raise ValueError(f"Unknown query type: {query.query_type!r}")
|
|
41
|
+
return handler(query)
|
|
42
|
+
|
|
43
|
+
def _compile_select(self, query: Query) -> CompiledQuery:
|
|
44
|
+
fields = "*" if not query.select_fields else ", ".join(query.select_fields)
|
|
45
|
+
sql = f"SELECT {fields} FROM {query.table}"
|
|
46
|
+
params: List[Any] = []
|
|
47
|
+
|
|
48
|
+
where, where_params = self._compile_where(query)
|
|
49
|
+
if where:
|
|
50
|
+
sql += f" WHERE {where}"
|
|
51
|
+
params.extend(where_params)
|
|
52
|
+
|
|
53
|
+
if query.order_by_clauses:
|
|
54
|
+
order = ", ".join(
|
|
55
|
+
f"{c.field} {'DESC' if c.descending else 'ASC'}"
|
|
56
|
+
for c in query.order_by_clauses
|
|
57
|
+
)
|
|
58
|
+
sql += f" ORDER BY {order}"
|
|
59
|
+
|
|
60
|
+
if query.limit_value is not None:
|
|
61
|
+
sql += " LIMIT ?"
|
|
62
|
+
params.append(query.limit_value)
|
|
63
|
+
elif query.offset_value is not None:
|
|
64
|
+
# SQLite requires LIMIT before OFFSET; -1 means no upper bound
|
|
65
|
+
sql += " LIMIT -1"
|
|
66
|
+
|
|
67
|
+
if query.offset_value is not None:
|
|
68
|
+
sql += " OFFSET ?"
|
|
69
|
+
params.append(query.offset_value)
|
|
70
|
+
|
|
71
|
+
return CompiledQuery(sql=sql, params=tuple(params), query=query)
|
|
72
|
+
|
|
73
|
+
def _compile_insert(self, query: Query) -> CompiledQuery:
|
|
74
|
+
columns = list(query.data.keys())
|
|
75
|
+
values = list(query.data.values())
|
|
76
|
+
placeholders = ", ".join(["?"] * len(columns))
|
|
77
|
+
sql = (
|
|
78
|
+
f"INSERT INTO {query.table} "
|
|
79
|
+
f"({', '.join(columns)}) VALUES ({placeholders})"
|
|
80
|
+
)
|
|
81
|
+
return CompiledQuery(sql=sql, params=tuple(values), query=query)
|
|
82
|
+
|
|
83
|
+
def _compile_update(self, query: Query) -> CompiledQuery:
|
|
84
|
+
pk_name = self._pk_field_name(query)
|
|
85
|
+
set_parts: List[str] = []
|
|
86
|
+
params: List[Any] = []
|
|
87
|
+
|
|
88
|
+
for col, val in query.data.items():
|
|
89
|
+
if col != pk_name:
|
|
90
|
+
set_parts.append(f"{col} = ?")
|
|
91
|
+
params.append(val)
|
|
92
|
+
|
|
93
|
+
sql = f"UPDATE {query.table} SET {', '.join(set_parts)}"
|
|
94
|
+
|
|
95
|
+
where, where_params = self._compile_where(query)
|
|
96
|
+
if where:
|
|
97
|
+
sql += f" WHERE {where}"
|
|
98
|
+
params.extend(where_params)
|
|
99
|
+
elif query.pk_value is not None:
|
|
100
|
+
sql += f" WHERE {pk_name} = ?"
|
|
101
|
+
params.append(query.pk_value)
|
|
102
|
+
|
|
103
|
+
return CompiledQuery(sql=sql, params=tuple(params), query=query)
|
|
104
|
+
|
|
105
|
+
def _compile_delete(self, query: Query) -> CompiledQuery:
|
|
106
|
+
pk_name = self._pk_field_name(query)
|
|
107
|
+
sql = f"DELETE FROM {query.table}"
|
|
108
|
+
params: List[Any] = []
|
|
109
|
+
|
|
110
|
+
where, where_params = self._compile_where(query)
|
|
111
|
+
if where:
|
|
112
|
+
sql += f" WHERE {where}"
|
|
113
|
+
params.extend(where_params)
|
|
114
|
+
elif query.pk_value is not None:
|
|
115
|
+
sql += f" WHERE {pk_name} = ?"
|
|
116
|
+
params.append(query.pk_value)
|
|
117
|
+
|
|
118
|
+
return CompiledQuery(sql=sql, params=tuple(params), query=query)
|
|
119
|
+
|
|
120
|
+
def _compile_where(self, query: Query) -> Tuple[str, List[Any]]:
|
|
121
|
+
if not query.conditions:
|
|
122
|
+
return "", []
|
|
123
|
+
|
|
124
|
+
parts: List[str] = []
|
|
125
|
+
params: List[Any] = []
|
|
126
|
+
|
|
127
|
+
for cond in query.conditions:
|
|
128
|
+
if cond.operator == "IN":
|
|
129
|
+
placeholders = ", ".join(["?"] * len(cond.value))
|
|
130
|
+
parts.append(f"{cond.field} IN ({placeholders})")
|
|
131
|
+
params.extend(cond.value)
|
|
132
|
+
elif cond.operator in ("IS NULL", "IS NOT NULL"):
|
|
133
|
+
parts.append(f"{cond.field} {cond.operator}")
|
|
134
|
+
else:
|
|
135
|
+
parts.append(f"{cond.field} {cond.operator} ?")
|
|
136
|
+
params.append(cond.value)
|
|
137
|
+
|
|
138
|
+
return " AND ".join(parts), params
|
|
139
|
+
|
|
140
|
+
def _pk_field_name(self, query: Query) -> str:
|
|
141
|
+
for name, f in query.model._fields.items():
|
|
142
|
+
if f.primary_key:
|
|
143
|
+
return name
|
|
144
|
+
return "id"
|
debugorm/core/manager.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Iterator, List, Optional, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from .query import Query, OrderByClause
|
|
5
|
+
from .pipeline import QueryPipeline
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .model import Model
|
|
9
|
+
from ..plugins.base import Plugin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DoesNotExist(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MultipleObjectsReturned(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class QuerySet:
|
|
21
|
+
"""
|
|
22
|
+
Lazy, chainable query builder.
|
|
23
|
+
|
|
24
|
+
Every filtering or ordering call returns a *new* ``QuerySet`` — the
|
|
25
|
+
original is never mutated. The database is only hit when results are
|
|
26
|
+
materialised via ``all()``, ``get()``, ``first()``, ``count()``, or
|
|
27
|
+
direct iteration.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
model: type,
|
|
33
|
+
pipeline: QueryPipeline,
|
|
34
|
+
query: Optional[Query] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._model = model
|
|
37
|
+
self._pipeline = pipeline
|
|
38
|
+
self._query = query if query is not None else Query(model)
|
|
39
|
+
|
|
40
|
+
def filter(self, **kwargs: Any) -> "QuerySet":
|
|
41
|
+
"""Append WHERE conditions. Multiple calls are AND-ed together."""
|
|
42
|
+
new_query = self._query.copy()
|
|
43
|
+
for lookup, value in kwargs.items():
|
|
44
|
+
new_query.add_filter(lookup, value)
|
|
45
|
+
return QuerySet(self._model, self._pipeline, new_query)
|
|
46
|
+
|
|
47
|
+
def order_by(self, *fields: str) -> "QuerySet":
|
|
48
|
+
"""
|
|
49
|
+
Order results. Prefix a field name with ``-`` for descending order::
|
|
50
|
+
|
|
51
|
+
User.objects.order_by('-age', 'name')
|
|
52
|
+
"""
|
|
53
|
+
new_query = self._query.copy()
|
|
54
|
+
for f in fields:
|
|
55
|
+
descending = f.startswith("-")
|
|
56
|
+
new_query.order_by_clauses.append(OrderByClause(f.lstrip("-"), descending))
|
|
57
|
+
return QuerySet(self._model, self._pipeline, new_query)
|
|
58
|
+
|
|
59
|
+
def limit(self, n: int) -> "QuerySet":
|
|
60
|
+
new_query = self._query.copy()
|
|
61
|
+
new_query.limit_value = n
|
|
62
|
+
return QuerySet(self._model, self._pipeline, new_query)
|
|
63
|
+
|
|
64
|
+
def offset(self, n: int) -> "QuerySet":
|
|
65
|
+
new_query = self._query.copy()
|
|
66
|
+
new_query.offset_value = n
|
|
67
|
+
return QuerySet(self._model, self._pipeline, new_query)
|
|
68
|
+
|
|
69
|
+
def all(self) -> List["Model"]:
|
|
70
|
+
"""Execute the query and return a list of model instances."""
|
|
71
|
+
result = self._pipeline.run(self._query)
|
|
72
|
+
return [self._model._from_row(row) for row in result.rows]
|
|
73
|
+
|
|
74
|
+
def get(self, **kwargs: Any) -> "Model":
|
|
75
|
+
"""Return exactly one object; raise if zero or multiple match."""
|
|
76
|
+
qs = self.filter(**kwargs) if kwargs else self
|
|
77
|
+
results = qs.all()
|
|
78
|
+
if not results:
|
|
79
|
+
raise self._model.DoesNotExist(
|
|
80
|
+
f"{self._model.__name__} matching query does not exist"
|
|
81
|
+
)
|
|
82
|
+
if len(results) > 1:
|
|
83
|
+
raise self._model.MultipleObjectsReturned(
|
|
84
|
+
f"get() returned more than one {self._model.__name__}"
|
|
85
|
+
)
|
|
86
|
+
return results[0]
|
|
87
|
+
|
|
88
|
+
def first(self) -> Optional["Model"]:
|
|
89
|
+
results = self.limit(1).all()
|
|
90
|
+
return results[0] if results else None
|
|
91
|
+
|
|
92
|
+
def count(self) -> int:
|
|
93
|
+
count_query = self._query.copy()
|
|
94
|
+
count_query.select_fields = ["COUNT(*) as count"]
|
|
95
|
+
count_query.limit_value = None
|
|
96
|
+
count_query.offset_value = None
|
|
97
|
+
count_query.order_by_clauses = []
|
|
98
|
+
result = self._pipeline.run(count_query)
|
|
99
|
+
return result.rows[0]["count"] if result.rows else 0
|
|
100
|
+
|
|
101
|
+
def exists(self) -> bool:
|
|
102
|
+
return self.count() > 0
|
|
103
|
+
|
|
104
|
+
def debug(
|
|
105
|
+
self,
|
|
106
|
+
explain: bool = False,
|
|
107
|
+
dry_run: bool = False,
|
|
108
|
+
log: bool = False,
|
|
109
|
+
visualize: bool = False,
|
|
110
|
+
pretty: bool = False,
|
|
111
|
+
) -> "QuerySet":
|
|
112
|
+
"""
|
|
113
|
+
Attach debug plugins on the fly::
|
|
114
|
+
|
|
115
|
+
User.objects.debug(explain=True, pretty=True).filter(age__gt=18).all()
|
|
116
|
+
"""
|
|
117
|
+
extra: List["Plugin"] = []
|
|
118
|
+
|
|
119
|
+
if visualize:
|
|
120
|
+
from ..plugins.visualize import VisualizePlugin
|
|
121
|
+
extra.append(VisualizePlugin())
|
|
122
|
+
if log:
|
|
123
|
+
from ..plugins.logging_plugin import LoggingPlugin
|
|
124
|
+
extra.append(LoggingPlugin())
|
|
125
|
+
if pretty:
|
|
126
|
+
from ..plugins.pretty_print import PrettyPrintPlugin
|
|
127
|
+
extra.append(PrettyPrintPlugin())
|
|
128
|
+
if explain:
|
|
129
|
+
from ..plugins.explain import ExplainPlugin
|
|
130
|
+
extra.append(ExplainPlugin())
|
|
131
|
+
if dry_run:
|
|
132
|
+
from ..plugins.dry_run import DryRunPlugin
|
|
133
|
+
extra.append(DryRunPlugin())
|
|
134
|
+
|
|
135
|
+
return QuerySet(self._model, self._pipeline.with_plugins(extra), self._query.copy())
|
|
136
|
+
|
|
137
|
+
def diff(self, other: "QuerySet") -> str:
|
|
138
|
+
"""
|
|
139
|
+
Compare two ``QuerySet`` objects at the internal representation level::
|
|
140
|
+
|
|
141
|
+
q1 = User.objects.filter(age__gt=18)
|
|
142
|
+
q2 = User.objects.filter(age__gt=21)
|
|
143
|
+
print(q1.diff(q2))
|
|
144
|
+
# - age > 18
|
|
145
|
+
# + age > 21
|
|
146
|
+
"""
|
|
147
|
+
return self._query.diff(other._query)
|
|
148
|
+
|
|
149
|
+
def __iter__(self) -> Iterator["Model"]:
|
|
150
|
+
return iter(self.all())
|
|
151
|
+
|
|
152
|
+
def __len__(self) -> int:
|
|
153
|
+
return self.count()
|
|
154
|
+
|
|
155
|
+
def __repr__(self) -> str:
|
|
156
|
+
return f"QuerySet<{self._model.__name__}>"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Manager:
|
|
160
|
+
"""
|
|
161
|
+
Entry point for all database queries on a model.
|
|
162
|
+
|
|
163
|
+
Attached to every ``Model`` subclass as ``Model.objects`` by ``ModelMeta``.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self, model_class: type) -> None:
|
|
167
|
+
self._model = model_class
|
|
168
|
+
self._pipeline: Optional[QueryPipeline] = None
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def pipeline(self) -> QueryPipeline:
|
|
172
|
+
if self._pipeline is None:
|
|
173
|
+
self._pipeline = QueryPipeline()
|
|
174
|
+
return self._pipeline
|
|
175
|
+
|
|
176
|
+
@pipeline.setter
|
|
177
|
+
def pipeline(self, value: QueryPipeline) -> None:
|
|
178
|
+
self._pipeline = value
|
|
179
|
+
|
|
180
|
+
def get_queryset(self) -> QuerySet:
|
|
181
|
+
return QuerySet(self._model, self.pipeline)
|
|
182
|
+
|
|
183
|
+
def filter(self, **kwargs: Any) -> QuerySet:
|
|
184
|
+
return self.get_queryset().filter(**kwargs)
|
|
185
|
+
|
|
186
|
+
def all(self) -> List["Model"]:
|
|
187
|
+
return self.get_queryset().all()
|
|
188
|
+
|
|
189
|
+
def get(self, **kwargs: Any) -> "Model":
|
|
190
|
+
return self.get_queryset().get(**kwargs)
|
|
191
|
+
|
|
192
|
+
def order_by(self, *fields: str) -> QuerySet:
|
|
193
|
+
return self.get_queryset().order_by(*fields)
|
|
194
|
+
|
|
195
|
+
def first(self) -> Optional["Model"]:
|
|
196
|
+
return self.get_queryset().first()
|
|
197
|
+
|
|
198
|
+
def count(self) -> int:
|
|
199
|
+
return self.get_queryset().count()
|
|
200
|
+
|
|
201
|
+
def create(self, **kwargs: Any) -> "Model":
|
|
202
|
+
instance = self._model(**kwargs)
|
|
203
|
+
instance.save()
|
|
204
|
+
return instance
|
|
205
|
+
|
|
206
|
+
def debug(self, **kwargs: Any) -> QuerySet:
|
|
207
|
+
return self.get_queryset().debug(**kwargs)
|
debugorm/core/model.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..fields.base import Field
|
|
5
|
+
from ..db.connection import get_connection
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ModelMeta(type):
|
|
12
|
+
"""
|
|
13
|
+
Metaclass that wires up fields, the table name, and the ``Manager``
|
|
14
|
+
when a ``Model`` subclass is defined.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __new__(
|
|
18
|
+
mcs,
|
|
19
|
+
name: str,
|
|
20
|
+
bases: Tuple[type, ...],
|
|
21
|
+
namespace: Dict[str, Any],
|
|
22
|
+
) -> "ModelMeta":
|
|
23
|
+
fields: Dict[str, Field] = {}
|
|
24
|
+
|
|
25
|
+
for base in bases:
|
|
26
|
+
if hasattr(base, "_fields"):
|
|
27
|
+
fields.update(base._fields)
|
|
28
|
+
|
|
29
|
+
for attr_name, attr_val in list(namespace.items()):
|
|
30
|
+
if isinstance(attr_val, Field):
|
|
31
|
+
attr_val.name = attr_name
|
|
32
|
+
fields[attr_name] = attr_val
|
|
33
|
+
|
|
34
|
+
namespace["_fields"] = fields
|
|
35
|
+
|
|
36
|
+
inner_meta = namespace.get("Meta")
|
|
37
|
+
if inner_meta is not None and hasattr(inner_meta, "table_name"):
|
|
38
|
+
namespace["_table_name"] = inner_meta.table_name
|
|
39
|
+
else:
|
|
40
|
+
namespace["_table_name"] = name.lower() + "s"
|
|
41
|
+
|
|
42
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
43
|
+
|
|
44
|
+
if name != "Model":
|
|
45
|
+
from .manager import Manager, DoesNotExist, MultipleObjectsReturned
|
|
46
|
+
|
|
47
|
+
cls.objects = Manager(cls)
|
|
48
|
+
cls.DoesNotExist = type("DoesNotExist", (DoesNotExist,), {})
|
|
49
|
+
cls.MultipleObjectsReturned = type(
|
|
50
|
+
"MultipleObjectsReturned", (MultipleObjectsReturned,), {}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return cls
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Model(metaclass=ModelMeta):
|
|
57
|
+
"""
|
|
58
|
+
Base class for all DebugORM models::
|
|
59
|
+
|
|
60
|
+
class User(Model):
|
|
61
|
+
id = IntegerField(primary_key=True)
|
|
62
|
+
name = StringField()
|
|
63
|
+
age = IntegerField()
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
_fields: Dict[str, Field] = {}
|
|
67
|
+
_table_name: str = ""
|
|
68
|
+
|
|
69
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
70
|
+
for field_name, field_obj in self._fields.items():
|
|
71
|
+
value = kwargs.get(field_name, field_obj.default)
|
|
72
|
+
object.__setattr__(self, field_name, value)
|
|
73
|
+
for key, value in kwargs.items():
|
|
74
|
+
if key not in self._fields:
|
|
75
|
+
object.__setattr__(self, key, value)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def _from_row(cls, row: Dict[str, Any]) -> "Model":
|
|
79
|
+
"""Reconstruct a model instance from a raw DB row dict."""
|
|
80
|
+
instance = cls.__new__(cls)
|
|
81
|
+
for field_name, field_obj in cls._fields.items():
|
|
82
|
+
object.__setattr__(instance, field_name, field_obj.from_db(row.get(field_name)))
|
|
83
|
+
return instance
|
|
84
|
+
|
|
85
|
+
def _pk_info(self) -> Tuple[Optional[str], Optional[Field]]:
|
|
86
|
+
for name, f in self._fields.items():
|
|
87
|
+
if f.primary_key:
|
|
88
|
+
return name, f
|
|
89
|
+
return None, None
|
|
90
|
+
|
|
91
|
+
def _pk_value(self) -> Any:
|
|
92
|
+
pk_name, _ = self._pk_info()
|
|
93
|
+
return getattr(self, pk_name, None) if pk_name else None
|
|
94
|
+
|
|
95
|
+
def save(self) -> None:
|
|
96
|
+
"""INSERT if this is a new instance, UPDATE if it already has a PK."""
|
|
97
|
+
from .query import Query
|
|
98
|
+
from .pipeline import QueryPipeline
|
|
99
|
+
|
|
100
|
+
pk_name, _ = self._pk_info()
|
|
101
|
+
pk_val = self._pk_value()
|
|
102
|
+
|
|
103
|
+
data: Dict[str, Any] = {}
|
|
104
|
+
for field_name, field_obj in self._fields.items():
|
|
105
|
+
value = getattr(self, field_name, None)
|
|
106
|
+
field_obj.validate(value)
|
|
107
|
+
if field_obj.primary_key and pk_val is None:
|
|
108
|
+
continue
|
|
109
|
+
data[field_name] = field_obj.to_db(value)
|
|
110
|
+
|
|
111
|
+
pipeline = QueryPipeline(connection=get_connection())
|
|
112
|
+
|
|
113
|
+
if pk_val is None:
|
|
114
|
+
query = Query(self.__class__)
|
|
115
|
+
query.query_type = "INSERT"
|
|
116
|
+
query.data = data
|
|
117
|
+
result = pipeline.run(query)
|
|
118
|
+
if pk_name and result.last_insert_id is not None:
|
|
119
|
+
object.__setattr__(self, pk_name, result.last_insert_id)
|
|
120
|
+
else:
|
|
121
|
+
query = Query(self.__class__)
|
|
122
|
+
query.query_type = "UPDATE"
|
|
123
|
+
query.data = data
|
|
124
|
+
query.pk_value = pk_val
|
|
125
|
+
pipeline.run(query)
|
|
126
|
+
|
|
127
|
+
def delete(self) -> None:
|
|
128
|
+
"""Delete this instance from the database and clear its primary key."""
|
|
129
|
+
from .query import Query
|
|
130
|
+
from .pipeline import QueryPipeline
|
|
131
|
+
|
|
132
|
+
pk_name, _ = self._pk_info()
|
|
133
|
+
pk_val = self._pk_value()
|
|
134
|
+
|
|
135
|
+
if pk_val is None:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"Cannot delete an unsaved {self.__class__.__name__} instance"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
query = Query(self.__class__)
|
|
141
|
+
query.query_type = "DELETE"
|
|
142
|
+
query.pk_value = pk_val
|
|
143
|
+
QueryPipeline(connection=get_connection()).run(query)
|
|
144
|
+
|
|
145
|
+
if pk_name:
|
|
146
|
+
object.__setattr__(self, pk_name, None)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def create_table(cls) -> None:
|
|
150
|
+
"""Execute ``CREATE TABLE IF NOT EXISTS`` for this model."""
|
|
151
|
+
col_defs = []
|
|
152
|
+
for field_name, field_obj in cls._fields.items():
|
|
153
|
+
col_def = f"{field_name} {field_obj.get_sql_type()}"
|
|
154
|
+
if field_obj.primary_key:
|
|
155
|
+
col_def += " PRIMARY KEY AUTOINCREMENT"
|
|
156
|
+
elif not field_obj.null:
|
|
157
|
+
col_def += " NOT NULL"
|
|
158
|
+
col_defs.append(col_def)
|
|
159
|
+
get_connection().create_table(cls._table_name, ", ".join(col_defs))
|
|
160
|
+
|
|
161
|
+
def __eq__(self, other: object) -> bool:
|
|
162
|
+
if not isinstance(other, self.__class__):
|
|
163
|
+
return NotImplemented
|
|
164
|
+
pk_name, _ = self._pk_info()
|
|
165
|
+
if pk_name:
|
|
166
|
+
return getattr(self, pk_name) == getattr(other, pk_name)
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
attrs = ", ".join(
|
|
171
|
+
f"{k}={getattr(self, k, None)!r}" for k in self._fields
|
|
172
|
+
)
|
|
173
|
+
return f"{self.__class__.__name__}({attrs})"
|