iceaxe 0.2.3.dev1__tar.gz → 0.2.3.dev2__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.
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/PKG-INFO +1 -1
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/test_queries.py +5 -1
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/test_session.py +24 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/base.py +8 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/queries.py +70 -15
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/schemas/db_memory_serializer.py +1 -6
- iceaxe-0.2.3.dev2/iceaxe/session_optimized.pyx +188 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/pyproject.toml +1 -1
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/setup.py +1 -1
- iceaxe-0.2.3.dev1/iceaxe/session_optimized.pyx +0 -102
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/LICENSE +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/README.md +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/build.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/.DS_Store +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/benchmarks/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/benchmarks/test_select.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/conf_models.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/conftest.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/migrations/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/migrations/conftest.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/migrations/test_action_sorter.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/migrations/test_generator.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/migrations/test_generics.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/mountaineer/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/mountaineer/dependencies/test_core.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/schemas/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/schemas/test_actions.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/schemas/test_cli.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/schemas/test_db_memory_serializer.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/schemas/test_db_serializer.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/schemas/test_db_stubs.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/test_base.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/test_comparison.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/test_field.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/comparison.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/field.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/functions.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/generics.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/io.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/logging.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/migrations/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/migrations/action_sorter.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/migrations/cli.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/migrations/client_io.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/migrations/generator.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/migrations/migration.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/migrations/migrator.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/mountaineer/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/mountaineer/cli.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/mountaineer/config.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/mountaineer/dependencies/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/mountaineer/dependencies/core.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/postgres.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/py.typed +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/queries_str.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/schemas/__init__.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/schemas/actions.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/schemas/cli.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/schemas/db_serializer.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/schemas/db_stubs.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/session.py +0 -0
- {iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/typing.py +0 -0
|
@@ -9,7 +9,11 @@ from iceaxe.queries import QueryBuilder, and_, or_, select
|
|
|
9
9
|
|
|
10
10
|
def test_select():
|
|
11
11
|
new_query = QueryBuilder().select(UserDemo)
|
|
12
|
-
assert new_query.build() == (
|
|
12
|
+
assert new_query.build() == (
|
|
13
|
+
'SELECT "userdemo"."id" as "userdemo_id", "userdemo"."name" as '
|
|
14
|
+
'"userdemo_name", "userdemo"."email" as "userdemo_email" FROM "userdemo"',
|
|
15
|
+
[],
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
def test_select_single_field():
|
|
@@ -281,6 +281,30 @@ async def test_select_join(db_connection: DBConnection):
|
|
|
281
281
|
]
|
|
282
282
|
|
|
283
283
|
|
|
284
|
+
@pytest.mark.asyncio
|
|
285
|
+
async def test_select_join_multiple_tables(db_connection: DBConnection):
|
|
286
|
+
user = UserDemo(name="John Doe", email="john@example.com")
|
|
287
|
+
await db_connection.insert([user])
|
|
288
|
+
assert user.id is not None
|
|
289
|
+
|
|
290
|
+
artifact = ArtifactDemo(title="Artifact 1", user_id=user.id)
|
|
291
|
+
await db_connection.insert([artifact])
|
|
292
|
+
|
|
293
|
+
new_query = (
|
|
294
|
+
QueryBuilder()
|
|
295
|
+
.select((ArtifactDemo, UserDemo))
|
|
296
|
+
.join(UserDemo, UserDemo.id == ArtifactDemo.user_id)
|
|
297
|
+
.where(UserDemo.name == "John Doe")
|
|
298
|
+
)
|
|
299
|
+
result = await db_connection.exec(new_query)
|
|
300
|
+
assert result == [
|
|
301
|
+
(
|
|
302
|
+
ArtifactDemo(id=artifact.id, title="Artifact 1", user_id=user.id),
|
|
303
|
+
UserDemo(id=user.id, name="John Doe", email="john@example.com"),
|
|
304
|
+
)
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
|
|
284
308
|
@pytest.mark.asyncio
|
|
285
309
|
async def test_select_with_limit_and_offset(db_connection: DBConnection):
|
|
286
310
|
users = [
|
|
@@ -136,3 +136,11 @@ class TableBase(BaseModel, metaclass=DBModelMetaclass):
|
|
|
136
136
|
if cls.table_name == PydanticUndefined:
|
|
137
137
|
return cls.__name__.lower()
|
|
138
138
|
return cls.table_name
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def get_client_fields(cls):
|
|
142
|
+
return {
|
|
143
|
+
field: info
|
|
144
|
+
for field, info in cls.model_fields.items()
|
|
145
|
+
if field not in INTERNAL_TABLE_FIELDS
|
|
146
|
+
}
|
|
@@ -29,17 +29,20 @@ from iceaxe.typing import (
|
|
|
29
29
|
|
|
30
30
|
P = TypeVar("P")
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
bound=TableBase
|
|
32
|
+
SUPPORTED_SELECTS = (
|
|
33
|
+
TableBase
|
|
35
34
|
| DBModelMetaclass
|
|
36
35
|
| ALL_ENUM_TYPES
|
|
37
36
|
| PRIMITIVE_TYPES
|
|
38
37
|
| PRIMITIVE_WRAPPER_TYPES
|
|
39
38
|
| DATE_TYPES
|
|
40
39
|
| JSON_WRAPPER_FALLBACK
|
|
41
|
-
| None
|
|
40
|
+
| None
|
|
42
41
|
)
|
|
42
|
+
|
|
43
|
+
T = TypeVar("T", bound=SUPPORTED_SELECTS)
|
|
44
|
+
T2 = TypeVar("T2", bound=SUPPORTED_SELECTS)
|
|
45
|
+
T3 = TypeVar("T3", bound=SUPPORTED_SELECTS)
|
|
43
46
|
Ts = TypeVarTuple("Ts")
|
|
44
47
|
|
|
45
48
|
|
|
@@ -102,19 +105,43 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
102
105
|
|
|
103
106
|
@overload
|
|
104
107
|
def select(
|
|
105
|
-
self, fields: tuple[T | Type[T]
|
|
106
|
-
) -> QueryBuilder[tuple[T
|
|
108
|
+
self, fields: tuple[T | Type[T]]
|
|
109
|
+
) -> QueryBuilder[tuple[T], Literal["SELECT"]]: ...
|
|
107
110
|
|
|
111
|
+
@overload
|
|
112
|
+
def select(
|
|
113
|
+
self, fields: tuple[T | Type[T], T2 | Type[T2]]
|
|
114
|
+
) -> QueryBuilder[tuple[T, T2], Literal["SELECT"]]: ...
|
|
115
|
+
|
|
116
|
+
@overload
|
|
108
117
|
def select(
|
|
109
|
-
self, fields: T | Type[T] |
|
|
118
|
+
self, fields: tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], *Ts]
|
|
119
|
+
) -> QueryBuilder[tuple[T, T2, T3, *Ts], Literal["SELECT"]]: ...
|
|
120
|
+
|
|
121
|
+
def select(
|
|
122
|
+
self,
|
|
123
|
+
fields: (
|
|
124
|
+
T
|
|
125
|
+
| Type[T]
|
|
126
|
+
| tuple[T | Type[T]]
|
|
127
|
+
| tuple[T | Type[T], T2 | Type[T2]]
|
|
128
|
+
| tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], *Ts]
|
|
129
|
+
),
|
|
110
130
|
) -> (
|
|
111
|
-
QueryBuilder[
|
|
112
|
-
| QueryBuilder[T, Literal["SELECT"]]
|
|
131
|
+
QueryBuilder[T, Literal["SELECT"]]
|
|
132
|
+
| QueryBuilder[tuple[T], Literal["SELECT"]]
|
|
133
|
+
| QueryBuilder[tuple[T, T2], Literal["SELECT"]]
|
|
134
|
+
| QueryBuilder[tuple[T, T2, T3, *Ts], Literal["SELECT"]]
|
|
113
135
|
):
|
|
114
136
|
"""
|
|
115
137
|
Creates a new select query for the given fields. Returns the same
|
|
116
138
|
QueryBuilder that is now flagged as a SELECT query.
|
|
117
139
|
|
|
140
|
+
Our select @overrides here support the required conversion from a table class (which
|
|
141
|
+
is specified as raw input) to individual instances which are returned. This is only
|
|
142
|
+
relevant for table classes since field selections should be 1:1 mirrored from the
|
|
143
|
+
request field annotation to the response type.
|
|
144
|
+
|
|
118
145
|
"""
|
|
119
146
|
all_fields: tuple[
|
|
120
147
|
DBFieldClassDefinition | Type[TableBase] | FunctionMetadata, ...
|
|
@@ -165,8 +192,15 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
165
192
|
self.select_raw.append(field)
|
|
166
193
|
elif is_base_table(field):
|
|
167
194
|
table_token = QueryIdentifier(field.get_table_name())
|
|
168
|
-
|
|
169
|
-
|
|
195
|
+
|
|
196
|
+
for field_name in field.get_client_fields():
|
|
197
|
+
field_token = QueryIdentifier(field_name)
|
|
198
|
+
return_field = QueryIdentifier(
|
|
199
|
+
f"{field.get_table_name()}_{field_name}"
|
|
200
|
+
)
|
|
201
|
+
self.select_fields.append(
|
|
202
|
+
QueryLiteral(f"{table_token}.{field_token} as {return_field}")
|
|
203
|
+
)
|
|
170
204
|
self.select_raw.append(field)
|
|
171
205
|
elif is_function_metadata(field):
|
|
172
206
|
field.local_name = f"aggregate_{self.select_aggregate_count}"
|
|
@@ -460,14 +494,35 @@ def select(fields: T | Type[T]) -> QueryBuilder[T, Literal["SELECT"]]: ...
|
|
|
460
494
|
|
|
461
495
|
@overload
|
|
462
496
|
def select(
|
|
463
|
-
fields: tuple[T | Type[T]
|
|
464
|
-
) -> QueryBuilder[tuple[T
|
|
497
|
+
fields: tuple[T | Type[T]],
|
|
498
|
+
) -> QueryBuilder[tuple[T], Literal["SELECT"]]: ...
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@overload
|
|
502
|
+
def select(
|
|
503
|
+
fields: tuple[T | Type[T], T2 | Type[T2]],
|
|
504
|
+
) -> QueryBuilder[tuple[T, T2], Literal["SELECT"]]: ...
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@overload
|
|
508
|
+
def select(
|
|
509
|
+
fields: tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], *Ts],
|
|
510
|
+
) -> QueryBuilder[tuple[T, T2, T3, *Ts], Literal["SELECT"]]: ...
|
|
465
511
|
|
|
466
512
|
|
|
467
513
|
def select(
|
|
468
|
-
fields:
|
|
514
|
+
fields: (
|
|
515
|
+
T
|
|
516
|
+
| Type[T]
|
|
517
|
+
| tuple[T | Type[T]]
|
|
518
|
+
| tuple[T | Type[T], T2 | Type[T2]]
|
|
519
|
+
| tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], *Ts]
|
|
520
|
+
),
|
|
469
521
|
) -> (
|
|
470
|
-
QueryBuilder[
|
|
522
|
+
QueryBuilder[T, Literal["SELECT"]]
|
|
523
|
+
| QueryBuilder[tuple[T], Literal["SELECT"]]
|
|
524
|
+
| QueryBuilder[tuple[T, T2], Literal["SELECT"]]
|
|
525
|
+
| QueryBuilder[tuple[T, T2, T3, *Ts], Literal["SELECT"]]
|
|
471
526
|
):
|
|
472
527
|
"""
|
|
473
528
|
Shortcut to create a SELECT query with a new QueryBuilder.
|
|
@@ -7,7 +7,6 @@ from uuid import UUID
|
|
|
7
7
|
from pydantic_core import PydanticUndefined
|
|
8
8
|
|
|
9
9
|
from iceaxe.base import (
|
|
10
|
-
INTERNAL_TABLE_FIELDS,
|
|
11
10
|
DBFieldInfo,
|
|
12
11
|
IndexConstraint,
|
|
13
12
|
TableBase,
|
|
@@ -244,11 +243,7 @@ class DatabaseHandler:
|
|
|
244
243
|
|
|
245
244
|
# Handle the columns
|
|
246
245
|
all_column_nodes: list[NodeDefinition] = []
|
|
247
|
-
for field_name, field in table.
|
|
248
|
-
# Only create user-columns
|
|
249
|
-
if field_name in INTERNAL_TABLE_FIELDS:
|
|
250
|
-
continue
|
|
251
|
-
|
|
246
|
+
for field_name, field in table.get_client_fields().items():
|
|
252
247
|
column_nodes = self._yield_nodes(
|
|
253
248
|
self.convert_column(field_name, field, table), dependencies=table_nodes
|
|
254
249
|
)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from typing import Any, List, Tuple
|
|
2
|
+
from iceaxe.base import TableBase
|
|
3
|
+
from iceaxe.queries import FunctionMetadata
|
|
4
|
+
from json import loads as json_loads
|
|
5
|
+
from cpython.ref cimport PyObject
|
|
6
|
+
from cpython.object cimport PyObject_GetItem
|
|
7
|
+
from libc.stdlib cimport malloc, free
|
|
8
|
+
from libc.string cimport memcpy
|
|
9
|
+
from cpython.ref cimport Py_INCREF, Py_DECREF
|
|
10
|
+
|
|
11
|
+
cdef struct FieldInfo:
|
|
12
|
+
char* name # Field name
|
|
13
|
+
char* select_attribute # Corresponding attribute in the select_raw
|
|
14
|
+
bint is_json # Flag indicating if the field is JSON
|
|
15
|
+
|
|
16
|
+
cdef char* allocate_cstring(bytes data):
|
|
17
|
+
cdef Py_ssize_t length = len(data)
|
|
18
|
+
cdef char* c_str = <char*>malloc((length + 1) * sizeof(char))
|
|
19
|
+
if not c_str:
|
|
20
|
+
raise MemoryError("Failed to allocate memory for C string.")
|
|
21
|
+
memcpy(c_str, <char*>data, length) # Cast bytes to char* for memcpy
|
|
22
|
+
c_str[length] = 0 # Null-terminate the string
|
|
23
|
+
return c_str
|
|
24
|
+
|
|
25
|
+
cdef void free_fields(FieldInfo** fields, Py_ssize_t* num_fields_array, Py_ssize_t num_selects):
|
|
26
|
+
cdef Py_ssize_t j, k
|
|
27
|
+
if fields:
|
|
28
|
+
for j in range(num_selects):
|
|
29
|
+
if fields[j]:
|
|
30
|
+
for k in range(num_fields_array[j]):
|
|
31
|
+
free(fields[j][k].name)
|
|
32
|
+
free(fields[j][k].select_attribute)
|
|
33
|
+
free(fields[j])
|
|
34
|
+
free(fields)
|
|
35
|
+
if num_fields_array:
|
|
36
|
+
free(num_fields_array)
|
|
37
|
+
|
|
38
|
+
cdef FieldInfo** precompute_fields(list select_raws, list select_types, Py_ssize_t num_selects, Py_ssize_t* num_fields_array):
|
|
39
|
+
cdef FieldInfo** fields = <FieldInfo**>malloc(num_selects * sizeof(FieldInfo*))
|
|
40
|
+
cdef Py_ssize_t j, k, num_fields
|
|
41
|
+
cdef dict field_dict
|
|
42
|
+
cdef bytes select_bytes, field_bytes
|
|
43
|
+
cdef char* c_select
|
|
44
|
+
cdef char* c_field
|
|
45
|
+
cdef object select_raw
|
|
46
|
+
cdef bint raw_is_table, raw_is_column, raw_is_function_metadata
|
|
47
|
+
|
|
48
|
+
if not fields:
|
|
49
|
+
raise MemoryError("Failed to allocate memory for fields.")
|
|
50
|
+
|
|
51
|
+
for j in range(num_selects):
|
|
52
|
+
select_raw = select_raws[j]
|
|
53
|
+
raw_is_table, raw_is_column, raw_is_function_metadata = select_types[j]
|
|
54
|
+
|
|
55
|
+
if raw_is_table:
|
|
56
|
+
field_dict = {field: info.is_json for field, info in select_raw.get_client_fields().items() if not info.exclude}
|
|
57
|
+
num_fields = len(field_dict)
|
|
58
|
+
num_fields_array[j] = num_fields
|
|
59
|
+
fields[j] = <FieldInfo*>malloc(num_fields * sizeof(FieldInfo))
|
|
60
|
+
if not fields[j]:
|
|
61
|
+
raise MemoryError("Failed to allocate memory for FieldInfo.")
|
|
62
|
+
|
|
63
|
+
for k, (field, is_json) in enumerate(field_dict.items()):
|
|
64
|
+
select_bytes = f"{select_raw.get_table_name()}_{field}".encode('utf-8')
|
|
65
|
+
c_select = allocate_cstring(select_bytes)
|
|
66
|
+
|
|
67
|
+
field_bytes = field.encode('utf-8')
|
|
68
|
+
c_field = allocate_cstring(field_bytes)
|
|
69
|
+
|
|
70
|
+
fields[j][k].select_attribute = c_select
|
|
71
|
+
fields[j][k].name = c_field
|
|
72
|
+
fields[j][k].is_json = is_json
|
|
73
|
+
else:
|
|
74
|
+
num_fields_array[j] = 0
|
|
75
|
+
fields[j] = NULL
|
|
76
|
+
|
|
77
|
+
return fields
|
|
78
|
+
|
|
79
|
+
cdef list process_values(
|
|
80
|
+
list values,
|
|
81
|
+
FieldInfo** fields,
|
|
82
|
+
Py_ssize_t* num_fields_array,
|
|
83
|
+
list select_raws,
|
|
84
|
+
list select_types,
|
|
85
|
+
Py_ssize_t num_selects
|
|
86
|
+
):
|
|
87
|
+
cdef Py_ssize_t num_values = len(values)
|
|
88
|
+
cdef list result_all = [None] * num_values
|
|
89
|
+
cdef Py_ssize_t i, j, k, num_fields
|
|
90
|
+
cdef PyObject** result_value
|
|
91
|
+
cdef object value, obj, item
|
|
92
|
+
cdef dict obj_dict
|
|
93
|
+
cdef bint raw_is_table, raw_is_column, raw_is_function_metadata
|
|
94
|
+
cdef char* field_name_c
|
|
95
|
+
cdef char* select_name_c
|
|
96
|
+
cdef str field_name
|
|
97
|
+
cdef str select_name
|
|
98
|
+
cdef object field_value
|
|
99
|
+
cdef object select_raw
|
|
100
|
+
cdef PyObject* temp_obj
|
|
101
|
+
|
|
102
|
+
for i in range(num_values):
|
|
103
|
+
value = values[i]
|
|
104
|
+
result_value = <PyObject**>malloc(num_selects * sizeof(PyObject*))
|
|
105
|
+
if not result_value:
|
|
106
|
+
raise MemoryError("Failed to allocate memory for result_value.")
|
|
107
|
+
try:
|
|
108
|
+
for j in range(num_selects):
|
|
109
|
+
select_raw = select_raws[j]
|
|
110
|
+
raw_is_table, raw_is_column, raw_is_function_metadata = select_types[j]
|
|
111
|
+
|
|
112
|
+
if raw_is_table:
|
|
113
|
+
obj_dict = {}
|
|
114
|
+
num_fields = num_fields_array[j]
|
|
115
|
+
for k in range(num_fields):
|
|
116
|
+
field_name_c = fields[j][k].name
|
|
117
|
+
select_name_c = fields[j][k].select_attribute
|
|
118
|
+
field_name = field_name_c.decode('utf-8')
|
|
119
|
+
select_name = select_name_c.decode('utf-8')
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
field_value = value[select_name] # Use Python dictionary access instead of PyObject_GetItem
|
|
123
|
+
except KeyError:
|
|
124
|
+
raise KeyError(f"Key '{select_name}' not found in value.")
|
|
125
|
+
|
|
126
|
+
if fields[j][k].is_json:
|
|
127
|
+
field_value = json_loads(field_value)
|
|
128
|
+
|
|
129
|
+
obj_dict[field_name] = field_value
|
|
130
|
+
|
|
131
|
+
obj = select_raw(**obj_dict)
|
|
132
|
+
result_value[j] = <PyObject*>obj
|
|
133
|
+
Py_INCREF(obj) # Increment reference count for the stored object
|
|
134
|
+
|
|
135
|
+
elif raw_is_column:
|
|
136
|
+
try:
|
|
137
|
+
item = value[select_raw.key] # Use Python dictionary access
|
|
138
|
+
except KeyError:
|
|
139
|
+
raise KeyError(f"Key '{select_raw.key}' not found in value.")
|
|
140
|
+
result_value[j] = <PyObject*>item
|
|
141
|
+
Py_INCREF(item)
|
|
142
|
+
|
|
143
|
+
elif raw_is_function_metadata:
|
|
144
|
+
try:
|
|
145
|
+
item = value[select_raw.local_name] # Use Python dictionary access
|
|
146
|
+
except KeyError:
|
|
147
|
+
raise KeyError(f"Key '{select_raw.local_name}' not found in value.")
|
|
148
|
+
result_value[j] = <PyObject*>item
|
|
149
|
+
Py_INCREF(item)
|
|
150
|
+
|
|
151
|
+
# Assemble the result
|
|
152
|
+
if num_selects == 1:
|
|
153
|
+
result_all[i] = <object>result_value[0]
|
|
154
|
+
Py_DECREF(<object>result_value[0]) # Decrement reference count
|
|
155
|
+
else:
|
|
156
|
+
result_tuple = tuple([<object>result_value[j] for j in range(num_selects)])
|
|
157
|
+
for j in range(num_selects):
|
|
158
|
+
Py_DECREF(<object>result_value[j]) # Decrement reference count for each item
|
|
159
|
+
result_all[i] = result_tuple
|
|
160
|
+
|
|
161
|
+
finally:
|
|
162
|
+
free(result_value)
|
|
163
|
+
|
|
164
|
+
return result_all
|
|
165
|
+
|
|
166
|
+
cdef list optimize_casting(list values, list select_raws, list select_types):
|
|
167
|
+
cdef Py_ssize_t num_selects = len(select_raws)
|
|
168
|
+
cdef Py_ssize_t* num_fields_array = <Py_ssize_t*>malloc(num_selects * sizeof(Py_ssize_t))
|
|
169
|
+
cdef FieldInfo** fields
|
|
170
|
+
cdef list result_all
|
|
171
|
+
|
|
172
|
+
if not num_fields_array:
|
|
173
|
+
raise MemoryError("Failed to allocate memory for num_fields_array.")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
fields = precompute_fields(select_raws, select_types, num_selects, num_fields_array)
|
|
177
|
+
result_all = process_values(values, fields, num_fields_array, select_raws, select_types, num_selects)
|
|
178
|
+
finally:
|
|
179
|
+
free_fields(fields, num_fields_array, num_selects)
|
|
180
|
+
|
|
181
|
+
return result_all
|
|
182
|
+
|
|
183
|
+
def optimize_exec_casting(
|
|
184
|
+
values: List[Any],
|
|
185
|
+
select_raws: List[Any],
|
|
186
|
+
select_types: List[Tuple[bool, bool, bool]]
|
|
187
|
+
) -> List[Any]:
|
|
188
|
+
return optimize_casting(values, select_raws, select_types)
|
|
@@ -22,7 +22,7 @@ install_requires = \
|
|
|
22
22
|
|
|
23
23
|
setup_kwargs = {
|
|
24
24
|
'name': 'iceaxe',
|
|
25
|
-
'version': '0.2.3.
|
|
25
|
+
'version': '0.2.3.dev2',
|
|
26
26
|
'description': 'A modern, fast ORM for Python.',
|
|
27
27
|
'long_description': '# iceaxe\n\nA modern, fast ORM for Python. We have the following goals:\n\n- 🏎️ **Performance**: We want to exceed or match the fastest ORMs in Python. We want our ORM\nto be as close as possible to raw-[asyncpg](https://github.com/MagicStack/asyncpg) speeds. See the "Benchmarks" section for more.\n- 📝 **Typehinting**: Everything should be typehinted with expected types. Declare your data as you\nexpect in Python and it should bidirectionally sync to the database.\n- 🐘 **Postgres only**: Leverage native Postgres features and simplify the implementation.\n- ⚡ **Common things are easy, rare things are possible**: 99% of the SQL queries we write are\nvanilla SELECT/INSERT/UPDATEs. These should be natively supported by your ORM. If you\'re writing _really_\ncomplex queries, these are better done by hand so you can see exactly what SQL will be run.\n\nIceaxe is in early alpha. It\'s also an independent project. It\'s compatible with the [Mountaineer](https://github.com/piercefreeman/mountaineer) ecosystem, but you can use it in whatever\nproject and web framework you\'re using.\n\n## Installation\n\nIf you\'re using poetry to manage your dependencies:\n\n```bash\npoetry add iceaxe\n```\n\nOtherwise install with pip:\n\n```bash\npip install iceaxe\n```\n\n## Usage\n\nDefine your models as a `TableBase` subclass:\n\n```python\nfrom iceaxe import TableBase\n\nclass Person(TableBase):\n id: int\n name: str\n age: int\n```\n\nTableBase is a subclass of Pydantic\'s `BaseModel`, so you get all of the validation and Field customization\nout of the box. We provide our own `Field` constructor that adds database-specific configuration. For instance, to make the\n`id` field a primary key / auto-incrementing you can do:\n\n```python\nfrom iceaxe import Field\n\nclass Person(TableBase):\n id: int = Field(primary_key=True)\n name: str\n age: int\n```\n\nOkay now you have a model. How do you interact with it?\n\nDatabases are based on a few core primitives to insert data, update it, and fetch it out again.\nTo do so you\'ll need a _database connection_, which is a connection over the network from your code\nto your Postgres database. The `DBConnection` is the core class for all ORM actions against the database.\n\n```python\nfrom iceaxe import DBConnection\nimport asyncpg\n\nconn = DBConnection(\n await asyncpg.connect(\n host="localhost",\n port=5432,\n user="db_user",\n password="yoursecretpassword",\n database="your_db",\n )\n)\n```\n\nThe Person class currently just lives in memory. To back it with a full\ndatabase table, we can run raw SQL or run a migration to add it:\n\n```python\nawait conn.conn.execute(\n """\n CREATE TABLE IF NOT EXISTS person (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL,\n age INT NOT NULL\n )\n """\n)\n```\n\n### Inserting Data\n\nInstantiate object classes as you normally do:\n\n```python\npeople = [\n Person(name="Alice", age=30),\n Person(name="Bob", age=40),\n Person(name="Charlie", age=50),\n]\nawait conn.insert(people)\n\nprint(people[0].id) # 1\nprint(people[1].id) # 2\n```\n\nBecause we\'re using an auto-incrementing primary key, the `id` field will be populated after the insert.\nIceaxe will automatically update the object in place with the newly assigned value.\n\n### Updating data\n\nNow that we have these lovely people, let\'s modify them.\n\n```python\nperson = people[0]\nperson.name = "Blice"\n```\n\nRight now, we have a Python object that\'s out of state with the database. But that\'s often okay. We can inspect it\nand further write logic - it\'s fully decoupled from the database.\n\n```python\ndef ensure_b_letter(person: Person):\n if person.name[0].lower() != "b":\n raise ValueError("Name must start with \'B\'")\n\nensure_b_letter(person)\n```\n\nTo sync the values back to the database, we can call `update`:\n\n```python\nawait conn.update([person])\n```\n\nIf we were to query the database directly, we see that the name has been updated:\n\n```\nid | name | age\n----+-------+-----\n 1 | Blice | 31\n 2 | Bob | 40\n 3 | Charlie | 50\n```\n\nBut no other fields have been touched. This lets a potentially concurrent process\nmodify `Alice`\'s record - say, updating the age to 31. By the time we update the data, we\'ll\nchange the name but nothing else. Under the hood we do this by tracking the fields that\nhave been modified in-memory and creating a targeted UPDATE to modify only those values.\n\n### Selecting data\n\nTo select data, we can use a `QueryBuilder`. For a shortcut to `select` query functions,\nyou can also just import select directly. This method takes the desired value parameters\nand returns a list of the desired objects.\n\n```python\nfrom iceaxe import select\n\nquery = select(Person).where(Person.name == "Blice", Person.age > 25)\nresults = await conn.exec(query)\n```\n\nIf we inspect the typing of `results`, we see that it\'s a `list[Person]` objects. This matches\nthe typehint of the `select` function. You can also target columns directly:\n\n```python\nquery = select((Person.id, Person.name)).where(Person.age > 25)\nresults = await conn.exec(query)\n```\n\nThis will return a list of tuples, where each tuple is the id and name of the person: `list[tuple[int, str]]`.\n\nWe support most of the common SQL operations. Just like the results, these are typehinted\nto their proper types as well. Static typecheckers and your IDE will throw an error if you try to compare\na string column to an integer, for instance. A more complex example of a query:\n\n```python\nquery = select((\n Person.id,\n FavoriteColor,\n)).join(\n FavoriteColor,\n Person.id == FavoriteColor.person_id,\n).where(\n Person.age > 25,\n Person.name == "Blice",\n).order_by(\n Person.age.desc(),\n).limit(10)\nresults = await conn.exec(query)\n```\n\nAs expected this will deliver results - and typehint - as a `list[tuple[int, FavoriteColor]]`\n\n## Production\n\n> [!IMPORTANT]\n> Iceaxe is in early alpha. We\'re using it internally and showly rolling out to our production\napplications, but we\'re not yet ready to recommend it for general use. The API and larger\nstability is subject to change.\n\nNote that underlying Postgres connection wrapped by `conn` will be alive for as long as your object is in memory. This uses up one\nof the allowable connections to your database. Your overall limit depends on your Postgres configuration\nor hosting provider, but most managed solutions top out around 150-300. If you need more concurrent clients\nconnected (and even if you don\'t - connection creation at the Postgres level is expensive), you can adopt\na load balancer like `pgbouncer` to better scale to traffic. More deployment notes to come.\n\nIt\'s also worth noting the absence of request pooling in this initialization. This is a feature of many ORMs that lets you limit\nthe overall connections you make to Postgres, and re-use these over time. We specifically don\'t offer request\npooling as part of Iceaxe, despite being supported by our underlying engine `asyncpg`. This is a bit more\naligned to how things should be structured in production. Python apps are always bound to one process thanks to\nthe GIL. So no matter what your connection pool will always be tied to the current Python process / runtime. When you\'re deploying onto a server with multiple cores, the pool will be duplicated across CPUs and largely defeats the purpose of capping\nnetwork connections in the first place.\n\n## Benchmarking\n\nWe have basic benchmarking tests in the `__tests__/benchmarks` directory. To run them, you\'ll need to execute the pytest suite:\n\n```bash\npoetry run pytest -m integration_tests\n```\n\nCurrent benchmarking as of October 11 2024 is:\n\n| | raw asyncpg | iceaxe | external overhead | |\n|-------------------|-------------|--------|-----------------------------------------------|---|\n| TableBase columns | 0.098s | 0.093s | | |\n| TableBase full | 0.164s | 1.345s | 10%: dict construction | 90%: pydantic overhead | |\n\n## Development\n\nIf you update your Cython implementation during development, you\'ll need to re-compile the Cython code. This can be done with\na simple poetry install. Poetry is set up to create a dynamic `setup.py` based on our `build.py` definition.\n\n```bash\npoetry install\n```\n\n## TODOs\n\n- [ ] Additional documentation with usage examples.\n',
|
|
28
28
|
'author': 'Pierce Freeman',
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# cython_optimizations.pyx
|
|
2
|
-
from typing import Any, List, Tuple, Type
|
|
3
|
-
from iceaxe.base import TableBase
|
|
4
|
-
from iceaxe.queries import FunctionMetadata
|
|
5
|
-
from json import loads as json_loads
|
|
6
|
-
from cpython.ref cimport PyObject
|
|
7
|
-
from cpython.object cimport PyObject_GetItem
|
|
8
|
-
from libc.stdlib cimport malloc, free
|
|
9
|
-
from libc.string cimport strcpy
|
|
10
|
-
|
|
11
|
-
cdef struct FieldInfo:
|
|
12
|
-
char* name
|
|
13
|
-
bint is_json
|
|
14
|
-
|
|
15
|
-
cdef list optimize_casting(list values, list select_raws, list select_types):
|
|
16
|
-
cdef:
|
|
17
|
-
Py_ssize_t i, j, k, num_values, num_selects
|
|
18
|
-
list result_all
|
|
19
|
-
PyObject **result_value
|
|
20
|
-
object value, obj, item
|
|
21
|
-
tuple select_type
|
|
22
|
-
bint raw_is_table, raw_is_column, raw_is_function_metadata
|
|
23
|
-
FieldInfo **fields
|
|
24
|
-
Py_ssize_t num_fields
|
|
25
|
-
dict field_dict
|
|
26
|
-
bytes field_bytes
|
|
27
|
-
char* c_field_name
|
|
28
|
-
|
|
29
|
-
num_values = len(values)
|
|
30
|
-
num_selects = len(select_raws)
|
|
31
|
-
result_all = [None] * num_values
|
|
32
|
-
|
|
33
|
-
# Pre-calculate field information
|
|
34
|
-
fields = <FieldInfo**>malloc(num_selects * sizeof(FieldInfo*))
|
|
35
|
-
if not fields:
|
|
36
|
-
raise MemoryError()
|
|
37
|
-
|
|
38
|
-
try:
|
|
39
|
-
for j in range(num_selects):
|
|
40
|
-
select_raw = select_raws[j]
|
|
41
|
-
raw_is_table, raw_is_column, raw_is_function_metadata = select_types[j]
|
|
42
|
-
if raw_is_table:
|
|
43
|
-
field_dict = {}
|
|
44
|
-
for field, info in select_raw.model_fields.items():
|
|
45
|
-
if not info.exclude:
|
|
46
|
-
field_dict[field] = info.is_json
|
|
47
|
-
num_fields = len(field_dict)
|
|
48
|
-
fields[j] = <FieldInfo*>malloc(num_fields * sizeof(FieldInfo))
|
|
49
|
-
if not fields[j]:
|
|
50
|
-
raise MemoryError()
|
|
51
|
-
for k, (field, is_json) in enumerate(field_dict.items()):
|
|
52
|
-
field_bytes = field.encode('utf-8')
|
|
53
|
-
c_field_name = <char*>malloc((len(field_bytes) + 1) * sizeof(char))
|
|
54
|
-
if not c_field_name:
|
|
55
|
-
raise MemoryError()
|
|
56
|
-
strcpy(c_field_name, field_bytes)
|
|
57
|
-
fields[j][k].name = c_field_name
|
|
58
|
-
fields[j][k].is_json = is_json
|
|
59
|
-
|
|
60
|
-
for i in range(num_values):
|
|
61
|
-
value = values[i]
|
|
62
|
-
result_value = <PyObject**>malloc(num_selects * sizeof(PyObject*))
|
|
63
|
-
if not result_value:
|
|
64
|
-
raise MemoryError()
|
|
65
|
-
try:
|
|
66
|
-
for j in range(num_selects):
|
|
67
|
-
select_raw = select_raws[j]
|
|
68
|
-
raw_is_table, raw_is_column, raw_is_function_metadata = select_types[j]
|
|
69
|
-
if raw_is_table:
|
|
70
|
-
obj_dict = {}
|
|
71
|
-
for k in range(num_fields):
|
|
72
|
-
field_name = fields[j][k].name.decode('utf-8')
|
|
73
|
-
field_value = PyObject_GetItem(value, field_name)
|
|
74
|
-
if fields[j][k].is_json:
|
|
75
|
-
field_value = json_loads(field_value)
|
|
76
|
-
obj_dict[field_name] = field_value
|
|
77
|
-
obj = select_raw(**obj_dict)
|
|
78
|
-
result_value[j] = <PyObject*>obj
|
|
79
|
-
elif raw_is_column:
|
|
80
|
-
item = PyObject_GetItem(value, select_raw.key)
|
|
81
|
-
result_value[j] = <PyObject*>item
|
|
82
|
-
elif raw_is_function_metadata:
|
|
83
|
-
item = PyObject_GetItem(value, select_raw.local_name)
|
|
84
|
-
result_value[j] = <PyObject*>item
|
|
85
|
-
if num_selects == 1:
|
|
86
|
-
result_all[i] = <object>result_value[0]
|
|
87
|
-
else:
|
|
88
|
-
result_all[i] = tuple([<object>result_value[j] for j in range(num_selects)])
|
|
89
|
-
finally:
|
|
90
|
-
free(result_value)
|
|
91
|
-
finally:
|
|
92
|
-
for j in range(num_selects):
|
|
93
|
-
if select_types[j][0]: # if raw_is_table
|
|
94
|
-
for k in range(num_fields):
|
|
95
|
-
free(fields[j][k].name)
|
|
96
|
-
free(fields[j])
|
|
97
|
-
free(fields)
|
|
98
|
-
|
|
99
|
-
return result_all
|
|
100
|
-
|
|
101
|
-
def optimize_exec_casting(values: List[Any], select_raw: List[Any], select_types: List[Tuple[bool, bool, bool]]) -> List[Any]:
|
|
102
|
-
return optimize_casting(values, select_raw, select_types)
|
|
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
|
{iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/mountaineer/dependencies/__init__.py
RENAMED
|
File without changes
|
{iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/mountaineer/dependencies/test_core.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{iceaxe-0.2.3.dev1 → iceaxe-0.2.3.dev2}/iceaxe/__tests__/schemas/test_db_memory_serializer.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|