fquery 0.2__tar.gz → 0.3__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.
- {fquery-0.2 → fquery-0.3}/PKG-INFO +1 -1
- fquery-0.3/fquery/sqlmodel.py +280 -0
- {fquery-0.2 → fquery-0.3}/fquery.egg-info/PKG-INFO +1 -1
- fquery-0.3/fquery.egg-info/requires.txt +10 -0
- {fquery-0.2 → fquery-0.3}/setup.py +7 -2
- {fquery-0.2 → fquery-0.3}/tests/benchmark.py +28 -2
- {fquery-0.2 → fquery-0.3}/tests/test_sqlmodel.py +32 -10
- fquery-0.2/fquery/sqlmodel.py +0 -96
- fquery-0.2/fquery.egg-info/requires.txt +0 -9
- {fquery-0.2 → fquery-0.3}/LICENSE +0 -0
- {fquery-0.2 → fquery-0.3}/README.md +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/__init__.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/aitertools.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/async_utils.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/django.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/execute.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/fgraphql.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/malloy_builder.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/query.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/resolve.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/sql_builder.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/view_model.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/visitor.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery/walk.py +0 -0
- {fquery-0.2 → fquery-0.3}/fquery.egg-info/SOURCES.txt +0 -0
- {fquery-0.2 → fquery-0.3}/fquery.egg-info/dependency_links.txt +0 -0
- {fquery-0.2 → fquery-0.3}/fquery.egg-info/top_level.txt +0 -0
- {fquery-0.2 → fquery-0.3}/pyproject.toml +0 -0
- {fquery-0.2 → fquery-0.3}/setup.cfg +0 -0
- {fquery-0.2 → fquery-0.3}/tests/__init__.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/async_test.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/django_example_model.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/graphql_mock_user.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/mock_user.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/test_malloy.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/test_operators.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/test_sql.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/test_walk.py +0 -0
- {fquery-0.2 → fquery-0.3}/tests/test_walk_obj.py +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from dataclasses import _FIELD, dataclass, field, fields, is_dataclass
|
|
3
|
+
from datetime import date, datetime, time
|
|
4
|
+
from typing import (
|
|
5
|
+
ClassVar,
|
|
6
|
+
ForwardRef,
|
|
7
|
+
List,
|
|
8
|
+
Optional,
|
|
9
|
+
Union,
|
|
10
|
+
get_args,
|
|
11
|
+
get_origin,
|
|
12
|
+
get_type_hints,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
import inflection
|
|
16
|
+
from sqlalchemy import (
|
|
17
|
+
Boolean,
|
|
18
|
+
Date,
|
|
19
|
+
DateTime,
|
|
20
|
+
Float,
|
|
21
|
+
Integer,
|
|
22
|
+
LargeBinary,
|
|
23
|
+
Sequence,
|
|
24
|
+
String,
|
|
25
|
+
Time,
|
|
26
|
+
)
|
|
27
|
+
from sqlalchemy.orm.base import Mapped
|
|
28
|
+
from sqlmodel import Column, Field, Relationship, SQLModel
|
|
29
|
+
|
|
30
|
+
SA_TYPEMAP = {
|
|
31
|
+
int: Integer,
|
|
32
|
+
int | None: Integer,
|
|
33
|
+
float: Float,
|
|
34
|
+
str: String,
|
|
35
|
+
bool: Boolean,
|
|
36
|
+
datetime: DateTime,
|
|
37
|
+
date: Date,
|
|
38
|
+
time: Time,
|
|
39
|
+
bytes: LargeBinary, # or Binary for smaller data
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
GLOBAL_ID_SEQ = Sequence("global_id_seq") # define sequence explicitly
|
|
43
|
+
SQL_PK = {"metadata": {"SQL": {"primary_key": True}}}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def unique():
|
|
47
|
+
return field(default=None, metadata={"SQL": {"unique": True}})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def foreign_key(name):
|
|
51
|
+
return field(
|
|
52
|
+
default=None,
|
|
53
|
+
metadata={
|
|
54
|
+
"SQL": {"relationship": True, "back_populates": False, "fk_name": name}
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def one_to_many():
|
|
60
|
+
return field(default=None, metadata={"SQL": {"relationship": True}})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def many_to_one(key_column=None, back_populates=None):
|
|
64
|
+
ret = field(
|
|
65
|
+
default=None, metadata={"SQL": {"relationship": True, "many_to_one": True}}
|
|
66
|
+
)
|
|
67
|
+
# if key_column is None, we default to {key_table_name}.id
|
|
68
|
+
if key_column is not None:
|
|
69
|
+
ret.metadata["SQL"]["key_column"] = key_column
|
|
70
|
+
if back_populates is not None:
|
|
71
|
+
ret.metadata["SQL"]["back_populates"] = back_populates
|
|
72
|
+
return ret
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def sqlmodel(cls):
|
|
76
|
+
return model()(dataclass(kw_only=True)(cls))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def model(table: bool = True, table_name: str = None, global_id: bool = False):
|
|
80
|
+
"""
|
|
81
|
+
A decorator that generates a SQLModel from a dataclass.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
table_name (str): The name of the database table. Defaults to the name of the dataclass.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A decorator that generates a SQLModel from a dataclass.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def sqlmodel(self) -> SQLModel:
|
|
91
|
+
attrs = {name: getattr(self, name) for name in self.__sqlmodel__.__fields__}
|
|
92
|
+
return self.__sqlmodel__(**attrs)
|
|
93
|
+
|
|
94
|
+
def get_field_def(cls, field) -> Union[Field, Relationship]:
|
|
95
|
+
sql_meta = field.metadata.get("SQL", {})
|
|
96
|
+
has_foreign_key = bool(sql_meta.get("foreign_key", None))
|
|
97
|
+
has_relationship = bool(sql_meta.get("relationship", None))
|
|
98
|
+
has_unique_constraint = sql_meta.get("unique", False)
|
|
99
|
+
if has_unique_constraint:
|
|
100
|
+
return Field(unique=True)
|
|
101
|
+
|
|
102
|
+
if not sql_meta or not (has_foreign_key or has_relationship):
|
|
103
|
+
sql_default_factory = field.default_factory
|
|
104
|
+
if isinstance(sql_default_factory, dataclasses._MISSING_TYPE):
|
|
105
|
+
sql_default_factory = None
|
|
106
|
+
return Field(
|
|
107
|
+
default_factory=sql_default_factory,
|
|
108
|
+
# TODO: revisit the idea of using string for unknown types
|
|
109
|
+
sa_column=Column(
|
|
110
|
+
SA_TYPEMAP.get(field.type, String),
|
|
111
|
+
GLOBAL_ID_SEQ if global_id else cls.id_seq,
|
|
112
|
+
primary_key=(
|
|
113
|
+
field.name == "id"
|
|
114
|
+
or field.metadata.get("SQL", {}).get("primary_key", False)
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
if has_relationship:
|
|
119
|
+
back_populates = sql_meta.get("back_populates", None)
|
|
120
|
+
if back_populates is False:
|
|
121
|
+
return Relationship()
|
|
122
|
+
if not back_populates:
|
|
123
|
+
back_populates = inflection.underscore(cls.__name__)
|
|
124
|
+
if sql_meta.get("many_to_one", False):
|
|
125
|
+
back_populates = inflection.pluralize(back_populates)
|
|
126
|
+
return Relationship(back_populates=back_populates)
|
|
127
|
+
if has_foreign_key:
|
|
128
|
+
return Field(default=None, foreign_key=sql_meta["foreign_key"])
|
|
129
|
+
raise "Unsupported case"
|
|
130
|
+
|
|
131
|
+
def get_field_type(field, cls):
|
|
132
|
+
sql_meta = field.metadata.get("SQL", {})
|
|
133
|
+
has_foreign_key = bool(sql_meta.get("foreign_key", None))
|
|
134
|
+
has_relationship = bool(sql_meta.get("relationship", None))
|
|
135
|
+
has_many_to_one_relationship = bool(sql_meta.get("many_to_one", None))
|
|
136
|
+
if has_foreign_key:
|
|
137
|
+
# Translate ClassName to id type.
|
|
138
|
+
# TODO: what if the id type is different?
|
|
139
|
+
return Optional[int]
|
|
140
|
+
if has_relationship:
|
|
141
|
+
type_class = field.type
|
|
142
|
+
other_class = type_class.__args__[0]
|
|
143
|
+
if has_many_to_one_relationship:
|
|
144
|
+
try:
|
|
145
|
+
type_class = get_type_hints(cls)[field.name]
|
|
146
|
+
except NameError:
|
|
147
|
+
# TODO: log exception?
|
|
148
|
+
pass
|
|
149
|
+
else:
|
|
150
|
+
return Optional[other_class.__sqlmodel__]
|
|
151
|
+
return field.type
|
|
152
|
+
|
|
153
|
+
def patch_back_populates_types(field, back_populates, cls, sqlmodel_cls):
|
|
154
|
+
sql_meta = field.metadata.get("SQL", {})
|
|
155
|
+
has_relationship = bool(sql_meta.get("relationship", None))
|
|
156
|
+
has_many_to_one_relationship = bool(sql_meta.get("many_to_one", None))
|
|
157
|
+
if has_relationship:
|
|
158
|
+
if has_many_to_one_relationship:
|
|
159
|
+
type_class = field.type
|
|
160
|
+
try:
|
|
161
|
+
type_class = get_type_hints(cls)[field.name]
|
|
162
|
+
except NameError:
|
|
163
|
+
# TODO: log exception?
|
|
164
|
+
pass
|
|
165
|
+
inner = type_class.__args__[0]
|
|
166
|
+
if isinstance(inner, ForwardRef):
|
|
167
|
+
# can't patch right now. Try at a later time via back_populates
|
|
168
|
+
return
|
|
169
|
+
other_class = inner.__sqlmodel__
|
|
170
|
+
old = other_class.__annotations__[back_populates]
|
|
171
|
+
# Should be sqlalchemy.orm.base.Mapped[typing.List[ForwardRef('T')]]
|
|
172
|
+
# replace it with Mapped[List[sqlmodel_cls]]
|
|
173
|
+
origin = get_origin(old)
|
|
174
|
+
inner = get_args(old)
|
|
175
|
+
if origin == Mapped and len(inner) and get_origin(inner[0]) is list:
|
|
176
|
+
other_class.__annotations__[back_populates] = Mapped[
|
|
177
|
+
List[sqlmodel_cls]
|
|
178
|
+
]
|
|
179
|
+
other_class.sqlmodel_rebuild()
|
|
180
|
+
|
|
181
|
+
# Replace Optional['T'] with Optional[TSQLModel]
|
|
182
|
+
old = field.type
|
|
183
|
+
origin = get_origin(old)
|
|
184
|
+
inner = get_args(old)
|
|
185
|
+
needs_rebuild = False
|
|
186
|
+
if origin == Union and len(inner) and inner[0] == ForwardRef(cls.__name__):
|
|
187
|
+
sqlmodel_cls.__annotations__[field.name] = Optional[sqlmodel_cls]
|
|
188
|
+
needs_rebuild = True
|
|
189
|
+
|
|
190
|
+
# Replace Optional[T] with Optional[TSQLModel] if T is a dataclass
|
|
191
|
+
if origin == Union and len(inner) and is_dataclass(inner[0]):
|
|
192
|
+
sqlmodel_cls.__annotations__[field.name] = Optional[inner[0].__sqlmodel__]
|
|
193
|
+
needs_rebuild = True
|
|
194
|
+
|
|
195
|
+
if needs_rebuild:
|
|
196
|
+
sqlmodel_cls.sqlmodel_rebuild()
|
|
197
|
+
|
|
198
|
+
def default_table_name(clsname: str) -> str:
|
|
199
|
+
return inflection.underscore(inflection.pluralize(clsname))
|
|
200
|
+
|
|
201
|
+
def decorator(cls):
|
|
202
|
+
# Check if the class is a dataclass
|
|
203
|
+
if not is_dataclass(cls):
|
|
204
|
+
raise ValueError("The class must be a dataclass")
|
|
205
|
+
|
|
206
|
+
nonlocal table_name
|
|
207
|
+
table_name = table_name or default_table_name(cls.__name__)
|
|
208
|
+
|
|
209
|
+
if not global_id:
|
|
210
|
+
cls.id_seq = Sequence(f"{table_name}_seq")
|
|
211
|
+
|
|
212
|
+
# Insert any foreign keys as necessary
|
|
213
|
+
for cfield in fields(cls):
|
|
214
|
+
sql_meta = cfield.metadata.get("SQL", {})
|
|
215
|
+
has_relationship = bool(sql_meta.get("relationship", None))
|
|
216
|
+
if has_relationship:
|
|
217
|
+
many_to_one = sql_meta.get("many_to_one", False)
|
|
218
|
+
foreign_key_name = cfield.name + "_id"
|
|
219
|
+
key_table_name = table_name
|
|
220
|
+
key_column_name = sql_meta.get("fk_name", f"{key_table_name}.id")
|
|
221
|
+
if many_to_one:
|
|
222
|
+
type_class = cfield.type
|
|
223
|
+
other_class = type_class.__args__[0]
|
|
224
|
+
if isinstance(other_class, ForwardRef):
|
|
225
|
+
other_class = other_class.__forward_arg__
|
|
226
|
+
other_class = getattr(other_class, "__name__", other_class)
|
|
227
|
+
key_table_name = default_table_name(other_class)
|
|
228
|
+
key_column_name = sql_meta.get(
|
|
229
|
+
"key_column_name", f"{key_table_name}.id"
|
|
230
|
+
)
|
|
231
|
+
back_populates = sql_meta.get("back_populates", None)
|
|
232
|
+
if back_populates is False or many_to_one:
|
|
233
|
+
|
|
234
|
+
new_field = field(
|
|
235
|
+
default=None,
|
|
236
|
+
metadata={"SQL": {"foreign_key": key_column_name}},
|
|
237
|
+
)
|
|
238
|
+
new_field._field_type = _FIELD
|
|
239
|
+
new_field.name = foreign_key_name
|
|
240
|
+
new_field.type = Optional[int]
|
|
241
|
+
cls.__dataclass_fields__[foreign_key_name] = new_field
|
|
242
|
+
setattr(cls, new_field.name, new_field.default)
|
|
243
|
+
|
|
244
|
+
# Generate the SQLModel class
|
|
245
|
+
sqlmodel_cls = type(
|
|
246
|
+
cls.__name__ + "SQLModel",
|
|
247
|
+
(SQLModel,),
|
|
248
|
+
{
|
|
249
|
+
# Table name is a plural and hence the 's' at the end
|
|
250
|
+
"__tablename__": table_name,
|
|
251
|
+
# Add type annotations to the generated fields
|
|
252
|
+
"__annotations__": {
|
|
253
|
+
**{field.name: get_field_type(field, cls) for field in fields(cls)},
|
|
254
|
+
**{
|
|
255
|
+
"Config": ClassVar,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
# pydantic wants this
|
|
259
|
+
"__module__": cls.__module__,
|
|
260
|
+
"Config": {"exclude": {"__sqlmodel__", "sqlmodel"}},
|
|
261
|
+
**{field.name: get_field_def(cls, field) for field in fields(cls)},
|
|
262
|
+
},
|
|
263
|
+
# For SQLModel's SQLModelMetaClass
|
|
264
|
+
table=table,
|
|
265
|
+
)
|
|
266
|
+
cls.__sqlmodel__ = sqlmodel_cls
|
|
267
|
+
# Update type annotations in any class with a relationship with this class to point
|
|
268
|
+
# to the SQLModel, not the dataclass
|
|
269
|
+
for cfield in fields(cls):
|
|
270
|
+
if not cfield.name in sqlmodel_cls.__sqlmodel_relationships__:
|
|
271
|
+
continue
|
|
272
|
+
rel = sqlmodel_cls.__sqlmodel_relationships__.get(cfield.name, None)
|
|
273
|
+
if rel and hasattr(rel, "back_populates"):
|
|
274
|
+
patch_back_populates_types(
|
|
275
|
+
cfield, rel.back_populates, cls, sqlmodel_cls
|
|
276
|
+
)
|
|
277
|
+
cls.sqlmodel = sqlmodel
|
|
278
|
+
return cls
|
|
279
|
+
|
|
280
|
+
return decorator
|
|
@@ -11,7 +11,7 @@ with open("README.md", "r") as fh:
|
|
|
11
11
|
|
|
12
12
|
setuptools.setup(
|
|
13
13
|
name="fquery",
|
|
14
|
-
version="0.
|
|
14
|
+
version="0.3",
|
|
15
15
|
description="A graph query engine",
|
|
16
16
|
url="https://github.com/adsharma/fquery",
|
|
17
17
|
long_description=long_description,
|
|
@@ -27,7 +27,12 @@ setuptools.setup(
|
|
|
27
27
|
install_requires=["aioitertools"],
|
|
28
28
|
test_requires=["sqlalchemy >= 2.0.36"],
|
|
29
29
|
extras_require={
|
|
30
|
-
"SQL": [
|
|
30
|
+
"SQL": [
|
|
31
|
+
"pypika >= 0.36.5",
|
|
32
|
+
"sqlmodel@git+https://github.com/adsharma/sqlmodel.git@sqlmodel_rebuild",
|
|
33
|
+
"duckdb_engine >= 0.14.0",
|
|
34
|
+
"inflection >= 0.5.1",
|
|
35
|
+
],
|
|
31
36
|
"graphql": ["strawberry >= 0.37.1"],
|
|
32
37
|
},
|
|
33
38
|
)
|
|
@@ -2,6 +2,8 @@ import timeit
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
5
7
|
from fquery.sqlmodel import model
|
|
6
8
|
|
|
7
9
|
|
|
@@ -11,8 +13,17 @@ class User:
|
|
|
11
13
|
id: int
|
|
12
14
|
name: str
|
|
13
15
|
email: str
|
|
14
|
-
created_at: datetime = None
|
|
15
|
-
updated_at: datetime = None
|
|
16
|
+
created_at: datetime | None = None
|
|
17
|
+
updated_at: datetime | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pydantic.dataclasses.dataclass
|
|
21
|
+
class UserPydantic:
|
|
22
|
+
id: int
|
|
23
|
+
name: str
|
|
24
|
+
email: str
|
|
25
|
+
created_at: datetime | None = None
|
|
26
|
+
updated_at: datetime | None = None
|
|
16
27
|
|
|
17
28
|
|
|
18
29
|
def create_user():
|
|
@@ -23,6 +34,10 @@ def create_user_sqlmodel():
|
|
|
23
34
|
return User(1, "John Doe", "john@example.com").sqlmodel()
|
|
24
35
|
|
|
25
36
|
|
|
37
|
+
def create_user_pydantic():
|
|
38
|
+
return UserPydantic(1, "John Doe", "john@example.com")
|
|
39
|
+
|
|
40
|
+
|
|
26
41
|
# Run the benchmark
|
|
27
42
|
num_iterations = 100000
|
|
28
43
|
time_taken = timeit.timeit(create_user, number=num_iterations)
|
|
@@ -40,3 +55,14 @@ print(f"Creating {num_iterations} User SQL Model objects took {time_taken:.6f} s
|
|
|
40
55
|
print(
|
|
41
56
|
f"Average time per object creation: {(time_taken / num_iterations) * 1e9:.2f} nanoseconds"
|
|
42
57
|
)
|
|
58
|
+
|
|
59
|
+
# Run the benchmark
|
|
60
|
+
num_iterations = 100000
|
|
61
|
+
time_taken = timeit.timeit(create_user_pydantic, number=num_iterations)
|
|
62
|
+
|
|
63
|
+
print(
|
|
64
|
+
f"Creating {num_iterations} User Pydantic Model objects took {time_taken:.6f} seconds"
|
|
65
|
+
)
|
|
66
|
+
print(
|
|
67
|
+
f"Average time per object creation: {(time_taken / num_iterations) * 1e9:.2f} nanoseconds"
|
|
68
|
+
)
|
|
@@ -1,25 +1,41 @@
|
|
|
1
|
-
from dataclasses import
|
|
1
|
+
from dataclasses import field
|
|
2
2
|
from datetime import datetime
|
|
3
|
+
from typing import List, Optional
|
|
3
4
|
|
|
4
5
|
from sqlalchemy import create_engine
|
|
5
6
|
from sqlalchemy.orm import sessionmaker
|
|
6
7
|
from sqlmodel import SQLModel
|
|
7
8
|
|
|
8
|
-
from fquery.sqlmodel import
|
|
9
|
+
from fquery.sqlmodel import (
|
|
10
|
+
SQL_PK,
|
|
11
|
+
foreign_key,
|
|
12
|
+
many_to_one,
|
|
13
|
+
one_to_many,
|
|
14
|
+
sqlmodel,
|
|
15
|
+
unique,
|
|
16
|
+
)
|
|
9
17
|
|
|
10
18
|
|
|
11
|
-
@
|
|
12
|
-
@dataclass
|
|
19
|
+
@sqlmodel
|
|
13
20
|
class User:
|
|
14
|
-
name: str
|
|
15
|
-
email: str
|
|
16
21
|
id: int | None = None
|
|
22
|
+
name: str
|
|
23
|
+
email: str = unique()
|
|
17
24
|
created_at: datetime = None
|
|
18
25
|
updated_at: datetime = None
|
|
19
26
|
|
|
27
|
+
friend: Optional["User"] = foreign_key("users.id")
|
|
28
|
+
reviews: List["Review"] = one_to_many()
|
|
29
|
+
|
|
20
30
|
|
|
21
|
-
@
|
|
22
|
-
|
|
31
|
+
@sqlmodel
|
|
32
|
+
class Review:
|
|
33
|
+
id: int | None = None
|
|
34
|
+
score: int
|
|
35
|
+
user: Optional[User] = many_to_one("users.id")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@sqlmodel
|
|
23
39
|
class Relation:
|
|
24
40
|
src: int | None = field(**SQL_PK)
|
|
25
41
|
type: int = field(**SQL_PK)
|
|
@@ -41,9 +57,9 @@ user1 = User(
|
|
|
41
57
|
email="jane@example.com",
|
|
42
58
|
created_at=datetime.now(),
|
|
43
59
|
updated_at=datetime.now(),
|
|
60
|
+
friend=user.id,
|
|
44
61
|
)
|
|
45
62
|
|
|
46
|
-
|
|
47
63
|
# The following is equivalent to: user.sql_model()
|
|
48
64
|
# from sqlmodel import Field
|
|
49
65
|
# from typing import Optional
|
|
@@ -70,7 +86,13 @@ def test_sqlmodel():
|
|
|
70
86
|
session.add(user1.sqlmodel())
|
|
71
87
|
session.commit()
|
|
72
88
|
|
|
73
|
-
relation = Relation(
|
|
89
|
+
relation = Relation(
|
|
90
|
+
src=user.id,
|
|
91
|
+
type=1,
|
|
92
|
+
dst=user1.id,
|
|
93
|
+
created_at=datetime.now(),
|
|
94
|
+
updated_at=datetime.now(),
|
|
95
|
+
)
|
|
74
96
|
session.add(relation.sqlmodel())
|
|
75
97
|
session.commit()
|
|
76
98
|
# Read all users from the database
|
fquery-0.2/fquery/sqlmodel.py
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
from dataclasses import fields, is_dataclass
|
|
2
|
-
from datetime import date, datetime, time
|
|
3
|
-
from typing import ClassVar
|
|
4
|
-
|
|
5
|
-
from sqlalchemy import (
|
|
6
|
-
Boolean,
|
|
7
|
-
Date,
|
|
8
|
-
DateTime,
|
|
9
|
-
Float,
|
|
10
|
-
Integer,
|
|
11
|
-
LargeBinary,
|
|
12
|
-
Sequence,
|
|
13
|
-
String,
|
|
14
|
-
Time,
|
|
15
|
-
)
|
|
16
|
-
from sqlmodel import Column, Field, SQLModel
|
|
17
|
-
|
|
18
|
-
SA_TYPEMAP = {
|
|
19
|
-
int: Integer,
|
|
20
|
-
int | None: Integer,
|
|
21
|
-
float: Float,
|
|
22
|
-
str: String,
|
|
23
|
-
bool: Boolean,
|
|
24
|
-
datetime: DateTime,
|
|
25
|
-
date: Date,
|
|
26
|
-
time: Time,
|
|
27
|
-
bytes: LargeBinary, # or Binary for smaller data
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
GLOBAL_ID_SEQ = Sequence("global_id_seq") # define sequence explicitly
|
|
31
|
-
SQL_PK = {"metadata": {"SQL": {"primary_key": True}}}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def model(table_name: str = None, global_id: bool = False):
|
|
35
|
-
"""
|
|
36
|
-
A decorator that generates a SQLModel from a dataclass.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
table_name (str): The name of the database table. Defaults to the name of the dataclass.
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
A decorator that generates a SQLModel from a dataclass.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
def sqlmodel(self) -> SQLModel:
|
|
46
|
-
return self.__sqlmodel__(**self.__dict__)
|
|
47
|
-
|
|
48
|
-
def decorator(cls):
|
|
49
|
-
# Check if the class is a dataclass
|
|
50
|
-
if not is_dataclass(cls):
|
|
51
|
-
raise ValueError("The class must be a dataclass")
|
|
52
|
-
|
|
53
|
-
# Generate the SQLModel class
|
|
54
|
-
sqlmodel_cls = type(
|
|
55
|
-
cls.__name__ + "SQLModel",
|
|
56
|
-
(SQLModel,),
|
|
57
|
-
{
|
|
58
|
-
# Table name is a plural and hence the 's' at the end
|
|
59
|
-
"__tablename__": table_name or cls.__name__.lower() + "s",
|
|
60
|
-
# Add type annotations to the generated fields
|
|
61
|
-
"__annotations__": {
|
|
62
|
-
**{field.name: field.type for field in fields(cls)},
|
|
63
|
-
**{
|
|
64
|
-
"Config": ClassVar,
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
# pydantic wants this
|
|
68
|
-
"__module__": cls.__module__,
|
|
69
|
-
"Config": {"exclude": {"__sqlmodel__", "sqlmodel"}},
|
|
70
|
-
**{
|
|
71
|
-
field.name: Field(
|
|
72
|
-
default_factory=getattr(cls, field.name, None),
|
|
73
|
-
# TODO: revisit the idea of using string for unknown types
|
|
74
|
-
sa_column=Column(
|
|
75
|
-
SA_TYPEMAP.get(field.type, String),
|
|
76
|
-
GLOBAL_ID_SEQ if global_id else None,
|
|
77
|
-
primary_key=(
|
|
78
|
-
field.name == "id"
|
|
79
|
-
or field.metadata.get("SQL", {}).get(
|
|
80
|
-
"primary_key", False
|
|
81
|
-
)
|
|
82
|
-
),
|
|
83
|
-
),
|
|
84
|
-
)
|
|
85
|
-
for field in fields(cls)
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
# For SQLModel's SQLModelMetaClass
|
|
89
|
-
table=True,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
cls.__sqlmodel__ = sqlmodel_cls
|
|
93
|
-
cls.sqlmodel = sqlmodel
|
|
94
|
-
return cls
|
|
95
|
-
|
|
96
|
-
return decorator
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|