sqlobjects 1.2.3__py3-none-any.whl → 1.2.4__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.
Files changed (21) hide show
  1. sqlobjects/fields/proxies.py +54 -35
  2. sqlobjects/fields/relations/descriptors.py +17 -23
  3. sqlobjects/fields/relations/prefetch.py +82 -164
  4. sqlobjects/fields/relations/utils.py +115 -172
  5. sqlobjects/metadata.py +1 -1
  6. sqlobjects/queryset.py +3 -0
  7. sqlobjects/session.py +12 -3
  8. {sqlobjects-1.2.3.dist-info → sqlobjects-1.2.4.dist-info}/METADATA +1 -1
  9. {sqlobjects-1.2.3.dist-info → sqlobjects-1.2.4.dist-info}/RECORD +21 -21
  10. {sqlobjects-1.2.3.data → sqlobjects-1.2.4.data}/data/share/sqlobjects/rules/01-database-session-guide.md +0 -0
  11. {sqlobjects-1.2.3.data → sqlobjects-1.2.4.data}/data/share/sqlobjects/rules/02-model-definition-guide.md +0 -0
  12. {sqlobjects-1.2.3.data → sqlobjects-1.2.4.data}/data/share/sqlobjects/rules/03-query-operations-guide.md +0 -0
  13. {sqlobjects-1.2.3.data → sqlobjects-1.2.4.data}/data/share/sqlobjects/rules/04-crud-operations-guide.md +0 -0
  14. {sqlobjects-1.2.3.data → sqlobjects-1.2.4.data}/data/share/sqlobjects/rules/05-relationships-guide.md +0 -0
  15. {sqlobjects-1.2.3.data → sqlobjects-1.2.4.data}/data/share/sqlobjects/rules/06-validation-signals-guide.md +0 -0
  16. {sqlobjects-1.2.3.data → sqlobjects-1.2.4.data}/data/share/sqlobjects/rules/07-performance-guide.md +0 -0
  17. {sqlobjects-1.2.3.data → sqlobjects-1.2.4.data}/data/share/sqlobjects/rules/README.md +0 -0
  18. {sqlobjects-1.2.3.dist-info → sqlobjects-1.2.4.dist-info}/WHEEL +0 -0
  19. {sqlobjects-1.2.3.dist-info → sqlobjects-1.2.4.dist-info}/entry_points.txt +0 -0
  20. {sqlobjects-1.2.3.dist-info → sqlobjects-1.2.4.dist-info}/licenses/LICENSE +0 -0
  21. {sqlobjects-1.2.3.dist-info → sqlobjects-1.2.4.dist-info}/top_level.txt +0 -0
@@ -96,6 +96,14 @@ class BaseRelated(Generic[T]):
96
96
  self.property = descriptor.property
97
97
  self._cached_value = None
98
98
  self._loaded = False
99
+ self._rel_info = None
100
+
101
+ def _get_relationship_info(self):
102
+ if self._rel_info is None:
103
+ from .relations.utils import RelationshipAnalyzer
104
+
105
+ self._rel_info = RelationshipAnalyzer.analyze_relationship(self.instance.__class__, self.property.name)
106
+ return self._rel_info
99
107
 
100
108
  async def fetch(self) -> T:
101
109
  """Fetch related object(s)."""
@@ -122,23 +130,28 @@ class RelatedObject(BaseRelated[T]):
122
130
 
123
131
  async def _load(self):
124
132
  """Load related object from database."""
125
- if self.property.foreign_keys and self.property.resolved_model:
126
- fk_field = self.property.foreign_keys
127
- if isinstance(fk_field, list):
128
- fk_field = fk_field[0]
133
+ if not self.property.resolved_model:
134
+ self._loaded = True
135
+ return
129
136
 
130
- fk_value = getattr(self.instance, fk_field)
131
- if fk_value is not None:
132
- related_table = self.property.resolved_model.get_table()
133
- pk_col = list(related_table.primary_key.columns)[0]
137
+ info = self._get_relationship_info()
138
+ if not info:
139
+ self._loaded = True
140
+ return
134
141
 
135
- query = select(related_table).where(pk_col == fk_value)
136
- session = self.instance.get_session()
137
- result = await session.execute(query)
138
- row = result.first()
142
+ fk_field = info["foreign_key_fields"][0]
143
+ ref_field = info["ref_fields"][0]
144
+ fk_value = getattr(self.instance, fk_field)
139
145
 
140
- if row:
141
- self._cached_value = self.property.resolved_model.from_dict(dict(row._mapping), validate=False)
146
+ if fk_value is not None:
147
+ related_table = self.property.resolved_model.get_table()
148
+ ref_col = related_table.c[ref_field]
149
+ query = select(related_table).where(ref_col == fk_value)
150
+ session = self.instance.get_session()
151
+ result = await session.execute(query)
152
+ row = result.first()
153
+ if row:
154
+ self._cached_value = self.property.resolved_model.from_dict(dict(row._mapping), validate=False)
142
155
 
143
156
  self._loaded = True
144
157
 
@@ -191,12 +204,18 @@ class OneToManyRelation(RelatedCollection[T]):
191
204
  self._set_empty_result()
192
205
  return
193
206
 
194
- instance_pk = self.instance.id
195
- related_table = self.property.resolved_model.get_table()
207
+ info = self._get_relationship_info()
208
+ if not info:
209
+ self._set_empty_result()
210
+ return
211
+
212
+ fk_field = info["foreign_key_fields"][0]
213
+ ref_field = info["ref_fields"][0]
214
+ ref_value = getattr(self.instance, ref_field)
196
215
 
197
- fk_name = self._get_fk_field()
198
- fk_col = related_table.c[fk_name]
199
- query = select(related_table).where(fk_col == instance_pk)
216
+ related_table = self.property.resolved_model.get_table()
217
+ fk_col = related_table.c[fk_field]
218
+ query = select(related_table).where(fk_col == ref_value)
200
219
  session = self.instance.get_session()
201
220
  result = await session.execute(query)
202
221
 
@@ -207,19 +226,32 @@ class OneToManyRelation(RelatedCollection[T]):
207
226
 
208
227
  async def add(self, *objs: T, session=None) -> None:
209
228
  """Add objects to the relationship."""
229
+ info = self._get_relationship_info()
230
+ if not info:
231
+ raise ValueError(
232
+ f"Cannot resolve relationship '{self.property.name}' on "
233
+ f"'{self.instance.__class__.__name__}'. Define it explicitly with relationship()."
234
+ )
210
235
  session = session or self.instance.get_session()
211
- fk_field = self._get_fk_field()
236
+ fk_field = info["foreign_key_fields"][0]
237
+ ref_value = getattr(self.instance, info["ref_fields"][0])
212
238
 
213
239
  for obj in objs:
214
- setattr(obj, fk_field, self.instance.id)
240
+ setattr(obj, fk_field, ref_value)
215
241
  await obj.using(session).save() # type: ignore
216
242
 
217
243
  self._invalidate_cache()
218
244
 
219
245
  async def remove(self, *objs: T, session=None) -> None:
220
246
  """Remove objects from the relationship."""
247
+ info = self._get_relationship_info()
248
+ if not info:
249
+ raise ValueError(
250
+ f"Cannot resolve relationship '{self.property.name}' on "
251
+ f"'{self.instance.__class__.__name__}'. Define it explicitly with relationship()."
252
+ )
221
253
  session = session or self.instance.get_session()
222
- fk_field = self._get_fk_field()
254
+ fk_field = info["foreign_key_fields"][0]
223
255
 
224
256
  for obj in objs:
225
257
  setattr(obj, fk_field, None)
@@ -227,19 +259,6 @@ class OneToManyRelation(RelatedCollection[T]):
227
259
 
228
260
  self._invalidate_cache()
229
261
 
230
- def _get_fk_field(self):
231
- """Get foreign key field name."""
232
- fk_name = self.property.foreign_keys
233
- if isinstance(fk_name, list):
234
- fk_name = fk_name[0]
235
- elif fk_name is None:
236
- fk_name = (
237
- f"{self.property.back_populates}_id"
238
- if self.property.back_populates
239
- else f"{self.instance.__class__.__name__.lower()}_id"
240
- )
241
- return fk_name
242
-
243
262
  def __str__(self):
244
263
  return f"<OneToManyRelation: {self.property.name}>"
245
264
 
@@ -3,6 +3,12 @@ from typing import TYPE_CHECKING, Generic, TypeVar, overload
3
3
  from ...cascade import OnDelete
4
4
 
5
5
 
6
+ def _normalize_fields(fields: str | list[str] | None) -> list[str] | None:
7
+ if fields is None:
8
+ return None
9
+ return [fields] if isinstance(fields, str) else list(fields)
10
+
11
+
6
12
  if TYPE_CHECKING:
7
13
  from ...model import ObjectModel
8
14
  from ..proxies import BaseRelated
@@ -27,6 +33,7 @@ class RelationshipProperty:
27
33
  self,
28
34
  argument: str | type["ObjectModel"],
29
35
  foreign_keys: str | list[str] | None = None,
36
+ remote_fields: str | list[str] | None = None,
30
37
  back_populates: str | None = None,
31
38
  backref: str | None = None,
32
39
  lazy: str = "select",
@@ -40,32 +47,21 @@ class RelationshipProperty:
40
47
  passive_deletes: bool = False,
41
48
  **kwargs,
42
49
  ):
43
- """Initialize relationship property with cascade and deletion behavior.
44
-
45
- Args:
46
- argument: Target model class or string name
47
- foreign_keys: Foreign key field name(s)
48
- back_populates: Name of reverse relationship attribute
49
- backref: Name for automatic reverse relationship
50
- lazy: Loading strategy ('select', 'dynamic', 'noload', 'raise')
51
- uselist: Whether relationship returns a list
52
- secondary: M2M table name
53
- primaryjoin: Custom primary join condition
54
- secondaryjoin: Custom secondary join condition for M2M
55
- order_by: Default ordering for collections
56
- cascade: Cascade behavior (bool for simple on/off, str for SQLAlchemy cascade options)
57
- on_delete: Behavior when related object is deleted
58
- passive_deletes: Whether to use passive deletes
59
- **kwargs: Additional relationship options
60
- """
50
+ if foreign_keys and remote_fields:
51
+ raise ValueError(
52
+ "Cannot specify both 'foreign_keys' and 'remote_fields'. "
53
+ "Use 'foreign_keys' when the FK is on this model, "
54
+ "'remote_fields' when the FK is on the related model."
55
+ )
61
56
  self.argument = argument
62
- self.foreign_keys = foreign_keys
57
+ self.foreign_keys: list[str] | None = _normalize_fields(foreign_keys)
58
+ self.remote_fields: list[str] | None = _normalize_fields(remote_fields)
63
59
  self.back_populates = back_populates
64
60
  self.backref = backref
65
61
  self.lazy = lazy
66
62
  self.uselist = uselist
67
63
  self.secondary = secondary
68
- self.m2m_definition = None # M2M table definition
64
+ self.m2m_definition = None
69
65
  self.primaryjoin = primaryjoin
70
66
  self.secondaryjoin = secondaryjoin
71
67
  self.order_by = order_by
@@ -75,9 +71,7 @@ class RelationshipProperty:
75
71
  self.name: str | None = None
76
72
  self.resolved_model: type[ObjectModel] | None = None
77
73
  self.relationship_type: str | None = None
78
- self.is_many_to_many: bool = False # M2M relationship flag
79
-
80
- # Store additional relationship configuration parameters
74
+ self.is_many_to_many: bool = False
81
75
  self.extra_kwargs = kwargs
82
76
 
83
77
 
@@ -1,3 +1,5 @@
1
+ from sqlalchemy import tuple_
2
+
1
3
  from .utils import RelationshipAnalyzer
2
4
 
3
5
 
@@ -8,19 +10,9 @@ class PrefetchHandler:
8
10
  self.session = session
9
11
 
10
12
  async def handle_prefetch_relationships(self, instances, prefetch_relationships):
11
- """Handle prefetch_related relationship prefetching.
12
-
13
- Args:
14
- instances: List of model instances
15
- prefetch_relationships: Set of relationship names to prefetch
16
-
17
- Returns:
18
- list: Instances with prefetched relationships attached
19
- """
20
13
  if not instances or not prefetch_relationships:
21
14
  return instances
22
15
 
23
- # Prefetch each relationship field
24
16
  for relationship_name in prefetch_relationships:
25
17
  relationship_info = RelationshipAnalyzer.analyze_relationship(instances[0].__class__, relationship_name)
26
18
  if relationship_info:
@@ -29,98 +21,81 @@ class PrefetchHandler:
29
21
  return instances
30
22
 
31
23
  async def _prefetch_single_relationship(self, instances, relationship_name, relationship_info):
32
- """Prefetch single relationship field."""
33
24
  rel_type = relationship_info["type"]
34
25
 
35
26
  if rel_type == "reverse_fk":
36
- await self._prefetch_reverse_foreign_key(instances, relationship_name, relationship_info)
27
+ await self._prefetch_by_fields(
28
+ instances,
29
+ relationship_name,
30
+ relationship_info["related_model"],
31
+ relationship_info["ref_fields"],
32
+ relationship_info["foreign_key_fields"],
33
+ [],
34
+ )
37
35
  elif rel_type == "one_to_one":
38
- await self._prefetch_one_to_one(instances, relationship_name, relationship_info)
36
+ info = relationship_info
37
+ await self._prefetch_by_fields(
38
+ instances,
39
+ relationship_name,
40
+ info["related_model"],
41
+ info["ref_fields"],
42
+ info["foreign_key_fields"],
43
+ None,
44
+ )
45
+ elif rel_type == "many_to_one":
46
+ info = relationship_info
47
+ await self._prefetch_by_fields(
48
+ instances,
49
+ relationship_name,
50
+ info["related_model"],
51
+ info["foreign_key_fields"],
52
+ info["ref_fields"],
53
+ None,
54
+ )
39
55
  elif rel_type == "many_to_many":
40
56
  await self._prefetch_many_to_many(instances, relationship_name, relationship_info)
41
- elif rel_type == "many_to_one":
42
- await self._prefetch_forward_foreign_key(instances, relationship_name, relationship_info)
43
-
44
- async def _prefetch_reverse_foreign_key(self, instances, relationship_name, relationship_info):
45
- """Prefetch reverse foreign key relationship (one-to-many)."""
46
- related_model = relationship_info["related_model"]
47
- foreign_key_field = relationship_info["foreign_key_field"]
48
- ref_field = relationship_info["ref_field"]
49
-
50
- # Collect reference values from main instances
51
- instance_values = [
52
- getattr(instance, ref_field)
53
- for instance in instances
54
- if hasattr(instance, ref_field) and getattr(instance, ref_field) is not None
55
- ]
56
- if not instance_values:
57
- # Still need to attach empty lists to all instances
58
- for instance in instances:
59
- instance._update_cache(relationship_name, [])
60
- return
61
57
 
62
- # Execute prefetch query
63
- related_objects = (
64
- await related_model.objects.using(self.session)
65
- .filter(getattr(related_model, foreign_key_field).in_(instance_values))
66
- .all()
67
- )
58
+ async def _prefetch_by_fields(
59
+ self, instances, relationship_name, related_model, lookup_fields, group_fields, empty
60
+ ):
61
+ """Unified prefetch for FK-based relationships."""
62
+ composite = len(lookup_fields) > 1
68
63
 
69
- # Group by foreign key value
70
- grouped_objects = {}
71
- for obj in related_objects:
72
- fk_value = getattr(obj, foreign_key_field, None)
73
- if fk_value is not None:
74
- if fk_value not in grouped_objects:
75
- grouped_objects[fk_value] = []
76
- grouped_objects[fk_value].append(obj)
77
-
78
- # Attach to main instances using unified proxy_cache
79
- for instance in instances:
80
- instance_value = getattr(instance, ref_field, None)
81
- related_list = grouped_objects.get(instance_value, [])
82
- instance._update_cache(relationship_name, related_list)
83
-
84
- async def _prefetch_one_to_one(self, instances, relationship_name, relationship_info):
85
- """Prefetch one-to-one relationship (reverse FK with uselist=False)."""
86
- related_model = relationship_info["related_model"]
87
- foreign_key_field = relationship_info["foreign_key_field"]
88
- ref_field = relationship_info["ref_field"]
64
+ def make_key(obj, fields):
65
+ return tuple(getattr(obj, f) for f in fields) if composite else getattr(obj, fields[0])
89
66
 
90
- # Collect reference values from main instances
91
- instance_values = [
92
- getattr(instance, ref_field)
93
- for instance in instances
94
- if hasattr(instance, ref_field) and getattr(instance, ref_field) is not None
67
+ lookup_values = [
68
+ make_key(inst, lookup_fields)
69
+ for inst in instances
70
+ if all(getattr(inst, f, None) is not None for f in lookup_fields)
95
71
  ]
96
- if not instance_values:
97
- # Still need to attach None to all instances
98
- for instance in instances:
99
- instance._update_cache(relationship_name, None)
72
+ if not lookup_values:
73
+ for inst in instances:
74
+ inst._update_cache(relationship_name, [] if isinstance(empty, list) else empty)
100
75
  return
101
76
 
102
- # Execute prefetch query
103
- related_objects = (
104
- await related_model.objects.using(self.session)
105
- .filter(getattr(related_model, foreign_key_field).in_(instance_values))
106
- .all()
107
- )
108
-
109
- # Create mapping (should be one-to-one)
110
- related_map = {}
111
- for obj in related_objects:
112
- fk_value = getattr(obj, foreign_key_field, None)
113
- if fk_value is not None:
114
- related_map[fk_value] = obj # Single object, not list
115
-
116
- # Attach to main instances using unified proxy_cache
117
- for instance in instances:
118
- instance_value = getattr(instance, ref_field, None)
119
- related_obj = related_map.get(instance_value, None) # Single object or None
120
- instance._update_cache(relationship_name, related_obj)
77
+ if composite:
78
+ cols = [getattr(related_model, f) for f in group_fields]
79
+ qs = related_model.objects.using(self.session).filter(tuple_(*cols).in_(lookup_values))
80
+ else:
81
+ qs = related_model.objects.using(self.session).filter(
82
+ getattr(related_model, group_fields[0]).in_(lookup_values)
83
+ )
84
+ related_objects = await qs.all()
85
+
86
+ if isinstance(empty, list):
87
+ grouped = {}
88
+ for obj in related_objects:
89
+ grouped.setdefault(make_key(obj, group_fields), []).append(obj)
90
+ for inst in instances:
91
+ inst._update_cache(relationship_name, grouped.get(make_key(inst, lookup_fields), []))
92
+ else:
93
+ related_map = {make_key(obj, group_fields): obj for obj in related_objects}
94
+ for inst in instances:
95
+ inst._update_cache(relationship_name, related_map.get(make_key(inst, lookup_fields)))
121
96
 
122
97
  async def _prefetch_many_to_many(self, instances, relationship_name, relationship_info):
123
- """Prefetch many-to-many relationship."""
98
+ """Prefetch many-to-many via through table."""
124
99
  related_model = relationship_info["related_model"]
125
100
  through_table = relationship_info["through_table"]
126
101
  left_field = relationship_info["left_field"]
@@ -128,114 +103,57 @@ class PrefetchHandler:
128
103
  left_ref_field = relationship_info["left_ref_field"]
129
104
  right_ref_field = relationship_info["right_ref_field"]
130
105
 
131
- # Collect reference values from main instances
132
106
  instance_values = [
133
- getattr(instance, left_ref_field)
134
- for instance in instances
135
- if hasattr(instance, left_ref_field) and getattr(instance, left_ref_field) is not None
107
+ getattr(inst, left_ref_field) for inst in instances if getattr(inst, left_ref_field, None) is not None
136
108
  ]
137
109
  if not instance_values:
138
- # Still need to attach empty lists to all instances
139
- for instance in instances:
140
- instance._update_cache(relationship_name, [])
110
+ for inst in instances:
111
+ inst._update_cache(relationship_name, [])
141
112
  return
142
113
 
143
- # Find through model
144
114
  through_model = self._find_through_model(through_table)
145
115
  if not through_model:
146
116
  return
147
117
 
148
- # Query through table associations
149
- through_objects = (
150
- await through_model.objects.using(self.session)
118
+ through_objects = await (
119
+ through_model.objects.using(self.session)
151
120
  .filter(getattr(through_model, left_field).in_(instance_values))
152
121
  .all()
153
122
  )
154
123
 
155
- # Collect related object reference values
156
124
  related_values = [getattr(obj, right_field) for obj in through_objects]
157
125
  if not related_values:
158
- # Still need to attach empty lists to all instances
159
- for instance in instances:
160
- instance._update_cache(relationship_name, [])
126
+ for inst in instances:
127
+ inst._update_cache(relationship_name, [])
161
128
  return
162
129
 
163
- # Query related objects
164
- related_objects = (
165
- await related_model.objects.using(self.session)
130
+ related_objects = await (
131
+ related_model.objects.using(self.session)
166
132
  .filter(getattr(related_model, right_ref_field).in_(related_values))
167
133
  .all()
168
134
  )
169
135
 
170
- # Create related object mapping
171
136
  related_map = {getattr(obj, right_ref_field): obj for obj in related_objects}
172
137
 
173
- # Group by main instance reference value
174
- grouped_relations = {}
138
+ grouped = {}
175
139
  for through_obj in through_objects:
176
- main_value = getattr(through_obj, left_field)
177
- related_value = getattr(through_obj, right_field)
178
-
179
- if main_value not in grouped_relations:
180
- grouped_relations[main_value] = []
181
-
182
- if related_value in related_map:
183
- grouped_relations[main_value].append(related_map[related_value])
184
-
185
- # Attach to main instances using unified proxy_cache
186
- for instance in instances:
187
- instance_value = getattr(instance, left_ref_field, None)
188
- related_list = grouped_relations.get(instance_value, [])
189
- instance._update_cache(relationship_name, related_list)
190
-
191
- async def _prefetch_forward_foreign_key(self, instances, relationship_name, relationship_info):
192
- """Prefetch forward foreign key relationship (many-to-one)."""
193
- related_model = relationship_info["related_model"]
194
- foreign_key_field = relationship_info["foreign_key_field"]
195
- ref_field = relationship_info["ref_field"]
196
-
197
- # Collect foreign key values
198
- fk_values = [
199
- getattr(instance, foreign_key_field)
200
- for instance in instances
201
- if hasattr(instance, foreign_key_field) and getattr(instance, foreign_key_field) is not None
202
- ]
203
- if not fk_values:
204
- # Still need to attach None to all instances
205
- for instance in instances:
206
- instance._update_cache(relationship_name, None)
207
- return
208
-
209
- # Query related objects
210
- related_objects = (
211
- await related_model.objects.using(self.session)
212
- .filter(getattr(related_model, ref_field).in_(fk_values))
213
- .all()
214
- )
215
-
216
- # Create mapping
217
- related_map = {getattr(obj, ref_field): obj for obj in related_objects}
140
+ main_val = getattr(through_obj, left_field)
141
+ rel_val = getattr(through_obj, right_field)
142
+ if rel_val in related_map:
143
+ grouped.setdefault(main_val, []).append(related_map[rel_val])
218
144
 
219
- # Attach to main instances using unified proxy_cache
220
- for instance in instances:
221
- fk_value = getattr(instance, foreign_key_field, None)
222
- if fk_value in related_map:
223
- instance._update_cache(relationship_name, related_map[fk_value])
224
- else:
225
- # Store None for instances without related objects
226
- instance._update_cache(relationship_name, None)
145
+ for inst in instances:
146
+ key = getattr(inst, left_ref_field, None)
147
+ inst._update_cache(relationship_name, grouped.get(key, []))
227
148
 
228
149
  @staticmethod
229
150
  def _find_through_model(through_table):
230
- """Find through table model."""
231
151
  from ...model import ObjectModel
232
152
 
233
153
  for subclass in ObjectModel.__subclasses__():
234
154
  try:
235
- if hasattr(subclass, "get_table"):
236
- table = subclass.get_table()
237
- if table.name == through_table:
238
- return subclass
155
+ if hasattr(subclass, "get_table") and subclass.get_table().name == through_table:
156
+ return subclass
239
157
  except Exception: # noqa
240
158
  continue
241
159
  return None
@@ -1,5 +1,6 @@
1
1
  from dataclasses import dataclass
2
- from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
2
+ from functools import lru_cache
3
+ from typing import TYPE_CHECKING, Any, TypeVar
3
4
 
4
5
  from sqlalchemy import Column, ForeignKey, Table
5
6
 
@@ -77,90 +78,26 @@ class RelationshipResolver:
77
78
 
78
79
  @staticmethod
79
80
  def resolve_relationship_type(property_: RelationshipProperty) -> str:
80
- """Automatically infer relationship type based on parameters.
81
-
82
- Args:
83
- property_: RelationshipProperty instance to analyze
84
-
85
- Returns:
86
- String representing the relationship type
87
- """
88
- # Handle explicit uselist setting
89
- if property_.uselist is False:
90
- return RelationshipType.MANY_TO_ONE if property_.foreign_keys else RelationshipType.ONE_TO_ONE
91
- elif property_.uselist:
92
- return RelationshipType.MANY_TO_MANY if property_.secondary else RelationshipType.ONE_TO_MANY
93
-
94
- # Auto-infer based on parameters
95
81
  if property_.secondary:
96
82
  property_.is_many_to_many = True
97
83
  property_.uselist = True
98
84
  return RelationshipType.MANY_TO_MANY
99
85
  elif property_.foreign_keys:
100
- property_.uselist = False
86
+ if property_.uselist is None:
87
+ property_.uselist = False
101
88
  return RelationshipType.MANY_TO_ONE
102
89
  else:
103
- property_.uselist = True
104
- return RelationshipType.ONE_TO_MANY
105
-
106
-
107
- @overload
108
- def relationship(
109
- argument: str | type,
110
- *,
111
- uselist: Literal[False],
112
- foreign_keys: str | list[str] | None = None,
113
- back_populates: str | None = None,
114
- backref: str | None = None,
115
- lazy: Literal["select"] = "select",
116
- primaryjoin: str | None = None,
117
- order_by: str | list[str] | None = None,
118
- cascade: CascadeType = None,
119
- passive_deletes: bool = False,
120
- **kwargs: Any,
121
- ) -> Any: ...
122
-
123
-
124
- @overload
125
- def relationship(
126
- argument: str | type,
127
- *,
128
- secondary: str | M2MTable,
129
- foreign_keys: str | list[str] | None = None,
130
- back_populates: str | None = None,
131
- backref: str | None = None,
132
- lazy: str = "select",
133
- uselist: bool | None = None,
134
- primaryjoin: str | None = None,
135
- secondaryjoin: str | None = None,
136
- order_by: str | list[str] | None = None,
137
- cascade: CascadeType = None,
138
- passive_deletes: bool = False,
139
- **kwargs: Any,
140
- ) -> Any: ...
141
-
142
-
143
- @overload
144
- def relationship(
145
- argument: str | type,
146
- *,
147
- foreign_keys: str | list[str] | None = None,
148
- back_populates: str | None = None,
149
- backref: str | None = None,
150
- lazy: str = "select",
151
- uselist: bool | None = None,
152
- primaryjoin: str | None = None,
153
- order_by: str | list[str] | None = None,
154
- cascade: CascadeType = None,
155
- passive_deletes: bool = False,
156
- **kwargs: Any,
157
- ) -> Any: ...
90
+ # remote_fields or no hint — one_to_many or one_to_one
91
+ if property_.uselist is None:
92
+ property_.uselist = True
93
+ return RelationshipType.ONE_TO_ONE if property_.uselist is False else RelationshipType.ONE_TO_MANY
158
94
 
159
95
 
160
96
  def relationship(
161
97
  argument: str | type["ObjectModel"],
162
98
  *,
163
99
  foreign_keys: str | list[str] | None = None,
100
+ remote_fields: str | list[str] | None = None,
164
101
  back_populates: str | None = None,
165
102
  backref: str | None = None,
166
103
  lazy: str = "select",
@@ -173,49 +110,30 @@ def relationship(
173
110
  passive_deletes: bool = False,
174
111
  **kwargs: Any,
175
112
  ) -> Any:
176
- """Define model relationship with SQLAlchemy-compatible cascade behavior.
113
+ """Define model relationship.
177
114
 
178
115
  Args:
179
116
  argument: Target model class or string name
180
- foreign_keys: Foreign key field name(s)
117
+ foreign_keys: FK field name(s) on this model (many_to_one side)
118
+ remote_fields: FK field name(s) on the related model (one_to_many/one_to_one side)
181
119
  back_populates: Name of reverse relationship attribute
182
120
  backref: Name for automatic reverse relationship
183
- lazy: Loading strategy ('select', 'dynamic', 'noload', 'raise')
121
+ lazy: Loading strategy
184
122
  uselist: Whether relationship returns a list
185
123
  secondary: M2M table name or M2MTable instance
186
124
  primaryjoin: Custom primary join condition
187
125
  secondaryjoin: Custom secondary join condition for M2M
188
126
  order_by: Default ordering for collections
189
- cascade: Application-layer cascade behavior (SQLAlchemy compatible)
127
+ cascade: Cascade behavior
190
128
  passive_deletes: Whether to use passive deletes
191
- **kwargs: Additional relationship options
192
-
193
- Returns:
194
- Column instance wrapping RelationshipDescriptor for type compatibility
195
-
196
- Raises:
197
- ValueError: If both back_populates and backref are specified
198
-
199
- Example:
200
- # With type annotation
201
- posts: Column[list["Post"]] = relationship("Post", back_populates="author")
202
- author: Column[User] = relationship("User", back_populates="posts")
203
-
204
- # With cascade
205
- posts = relationship("Post", cascade={CascadeOption.ALL, CascadeOption.DELETE_ORPHAN})
206
129
  """
207
-
208
- # Validate mutually exclusive parameters
209
130
  if back_populates and backref:
210
131
  raise ValueError("Cannot specify both 'back_populates' and 'backref'")
211
132
 
212
- # Normalize cascade parameter to SQLAlchemy string format
213
133
  cascade_str = normalize_cascade(cascade)
214
134
 
215
- # Handle M2M table definition
216
135
  secondary_table_name = None
217
136
  m2m_def = None
218
-
219
137
  if isinstance(secondary, M2MTable):
220
138
  m2m_def = secondary
221
139
  secondary_table_name = secondary.table_name
@@ -225,6 +143,7 @@ def relationship(
225
143
  property_ = RelationshipProperty(
226
144
  argument=argument,
227
145
  foreign_keys=foreign_keys,
146
+ remote_fields=remote_fields,
228
147
  back_populates=back_populates,
229
148
  backref=backref,
230
149
  lazy=lazy,
@@ -233,20 +152,17 @@ def relationship(
233
152
  primaryjoin=primaryjoin,
234
153
  secondaryjoin=secondaryjoin,
235
154
  order_by=order_by,
236
- cascade=cascade_str, # Use normalized string
155
+ cascade=cascade_str,
237
156
  passive_deletes=passive_deletes,
238
157
  **kwargs,
239
158
  )
240
159
 
241
- # Set M2M definition if provided
242
160
  if m2m_def:
243
161
  property_.m2m_definition = m2m_def # type: ignore[reportAttributeAccessIssue]
244
162
  property_.is_many_to_many = True
245
163
 
246
- # Import Related here to avoid circular import
247
164
  from ..core import Related
248
165
 
249
- # Return Related container for relationship fields
250
166
  return Related(is_relationship=True, relationship_property=property_, m2m_definition=m2m_def)
251
167
 
252
168
 
@@ -254,117 +170,147 @@ class RelationshipAnalyzer:
254
170
  """Analyze model relationships and extract metadata for prefetch operations."""
255
171
 
256
172
  @staticmethod
173
+ @lru_cache(maxsize=256)
257
174
  def analyze_relationship(model_class, relationship_name):
258
- """Analyze relationship type and extract related information.
259
-
260
- Args:
261
- model_class: Main model class
262
- relationship_name: Relationship field name
263
-
264
- Returns:
265
- dict: Relationship info dict with type, related model, field mappings
266
- """
267
175
  try:
268
- # Check explicit relationship definition
269
176
  if hasattr(model_class, relationship_name):
270
177
  field_attr = getattr(model_class, relationship_name)
271
178
  if hasattr(field_attr, "property"):
272
179
  return RelationshipAnalyzer._extract_relationship_info(model_class, field_attr.property)
273
-
274
- # Infer reverse relationship
275
180
  return RelationshipAnalyzer._infer_reverse_relationship(model_class, relationship_name)
276
181
  except Exception: # noqa
277
182
  return None
278
183
 
279
184
  @staticmethod
280
185
  def _extract_relationship_info(model_class, prop):
281
- """Extract information from relationship property."""
282
186
  related_model = RelationshipAnalyzer._resolve_model_class(prop.argument)
283
187
  if not related_model:
284
188
  return None
285
189
 
286
- if prop.secondary: # Many-to-many relationship
190
+ # many_to_many
191
+ if prop.secondary:
287
192
  m2m_def = getattr(prop, "m2m_definition", None)
288
- if m2m_def:
289
- return {
290
- "type": "many_to_many",
291
- "related_model": related_model,
292
- "through_table": prop.secondary,
293
- "left_field": m2m_def.left_field,
294
- "right_field": m2m_def.right_field,
295
- "left_ref_field": m2m_def.left_ref_field,
296
- "right_ref_field": m2m_def.right_ref_field,
297
- }
298
- else:
299
- # String-only secondary table - cannot determine field mappings without M2MTable definition
193
+ if not m2m_def:
300
194
  return None
195
+ return {
196
+ "type": "many_to_many",
197
+ "related_model": related_model,
198
+ "through_table": prop.secondary,
199
+ "left_field": m2m_def.left_field,
200
+ "right_field": m2m_def.right_field,
201
+ "left_ref_field": m2m_def.left_ref_field,
202
+ "right_ref_field": m2m_def.right_ref_field,
203
+ }
301
204
 
302
- elif prop.foreign_keys: # Many-to-one (forward foreign key)
205
+ # many_to_one FK is on this model
206
+ if prop.foreign_keys:
207
+ from .descriptors import _normalize_fields
208
+
209
+ fks = _normalize_fields(prop.foreign_keys)
210
+ ref_fields = RelationshipAnalyzer._scan_ref_fields(model_class, fks, related_model)
303
211
  return {
304
212
  "type": "many_to_one",
305
213
  "related_model": related_model,
306
- "foreign_key_field": prop.foreign_keys,
307
- "ref_field": RelationshipAnalyzer._extract_ref_field(prop.foreign_keys),
214
+ "foreign_key_fields": fks,
215
+ "ref_fields": ref_fields,
308
216
  }
309
217
 
310
- else: # One-to-many or one-to-one (reverse relationship)
311
- # Try to find the correct foreign key field from back_populates
312
- foreign_key_field = f"{model_class.__name__.lower()}_id" # Default
218
+ # one_to_many / one_to_one FK is on the related model
219
+ rel_type = "one_to_one" if prop.uselist is False else "reverse_fk"
313
220
 
314
- if prop.back_populates:
315
- # Look for the corresponding relationship in the related model
316
- if hasattr(related_model, prop.back_populates):
317
- back_attr = getattr(related_model, prop.back_populates)
318
- if hasattr(back_attr, "property") and back_attr.property.foreign_keys:
319
- foreign_key_field = back_attr.property.foreign_keys
221
+ if prop.remote_fields:
222
+ from .descriptors import _normalize_fields
320
223
 
321
- # Determine if it's one-to-one or one-to-many based on uselist
322
- rel_type = "one_to_one" if prop.uselist is False else "reverse_fk"
224
+ rf = prop.remote_fields
225
+ foreign_key_fields = _normalize_fields(rf) if isinstance(rf, (str, list)) else None
226
+ if not foreign_key_fields:
227
+ foreign_key_fields, ref_fields = RelationshipAnalyzer._scan_fk_fields(related_model, model_class, prop)
228
+ else:
229
+ ref_fields = RelationshipAnalyzer._scan_ref_fields(related_model, foreign_key_fields, model_class)
230
+ else:
231
+ foreign_key_fields, ref_fields = RelationshipAnalyzer._scan_fk_fields(related_model, model_class, prop)
323
232
 
324
- return {
325
- "type": rel_type,
326
- "related_model": related_model,
327
- "foreign_key_field": foreign_key_field,
328
- "ref_field": "id",
329
- }
233
+ return {
234
+ "type": rel_type,
235
+ "related_model": related_model,
236
+ "foreign_key_fields": foreign_key_fields,
237
+ "ref_fields": ref_fields,
238
+ }
239
+
240
+ @staticmethod
241
+ def _scan_fk_fields(related_model, current_model, prop):
242
+ """Scan related model's columns to find FK(s) pointing to current model's table."""
243
+ try:
244
+ current_table = current_model.__table__
245
+ related_table = related_model.__table__
246
+ except AttributeError:
247
+ raise ValueError(
248
+ f"Cannot resolve relationship: model tables not available. "
249
+ f"Use 'remote_fields' to specify the FK field on {related_model.__name__}."
250
+ ) from None
251
+
252
+ candidates = [
253
+ (col.name, [fk.column.name for fk in col.foreign_keys if fk.column.table == current_table])
254
+ for col in related_table.columns
255
+ if any(fk.column.table == current_table for fk in col.foreign_keys)
256
+ ]
257
+
258
+ if not candidates:
259
+ # Fallback: try back_populates
260
+ if prop.back_populates and hasattr(related_model, prop.back_populates):
261
+ back_attr = getattr(related_model, prop.back_populates)
262
+ if hasattr(back_attr, "property") and back_attr.property.foreign_keys:
263
+ fks = back_attr.property.foreign_keys
264
+ refs = [RelationshipAnalyzer._extract_ref_field(fk) for fk in fks]
265
+ return fks, refs
266
+ raise ValueError(
267
+ f"No foreign key found on '{related_model.__name__}' pointing to '{current_model.__name__}'. "
268
+ f"Use 'remote_fields' to specify the FK field."
269
+ )
270
+
271
+ if len(candidates) > 1:
272
+ names = [c[0] for c in candidates]
273
+ raise ValueError(
274
+ f"Multiple foreign keys found on '{related_model.__name__}' pointing to "
275
+ f"'{current_model.__name__}': {names}. Use 'remote_fields' to specify which one."
276
+ )
277
+
278
+ fk_col_name, ref_col_names = candidates[0]
279
+ return [fk_col_name], ref_col_names if ref_col_names else ["id"]
280
+
281
+ @staticmethod
282
+ def _scan_ref_fields(related_model, foreign_key_fields, current_model):
283
+ """Given FK field names on related model, find the referenced columns on current model."""
284
+ try:
285
+ related_table = related_model.__table__
286
+ except AttributeError:
287
+ return ["id"] * len(foreign_key_fields)
288
+
289
+ ref_fields = []
290
+ for fk_name in foreign_key_fields:
291
+ if fk_name in related_table.c:
292
+ col = related_table.c[fk_name]
293
+ refs = [fk.column.name for fk in col.foreign_keys]
294
+ ref_fields.append(refs[0] if refs else "id")
295
+ else:
296
+ ref_fields.append("id")
297
+ return ref_fields
330
298
 
331
299
  @staticmethod
332
300
  def _extract_ref_field(foreign_key_spec):
333
- """Extract reference field from foreign key specification."""
334
301
  if isinstance(foreign_key_spec, str) and "." in foreign_key_spec:
335
302
  return foreign_key_spec.split(".", 1)[1]
336
- return "id" # Default to primary key
303
+ return foreign_key_spec if isinstance(foreign_key_spec, str) else "id"
337
304
 
338
305
  @staticmethod
339
306
  def _infer_reverse_relationship(model_class, relationship_name):
340
- """Infer reverse relationship (e.g., User.posts)."""
341
- # posts -> Post, comments -> Comment
342
- related_model_name = relationship_name.rstrip("s").capitalize()
343
- related_model = RelationshipAnalyzer._resolve_model_class(related_model_name)
344
-
345
- if related_model:
346
- # Check if related model has foreign key pointing to current model
347
- foreign_key_field = f"{model_class.__name__.lower()}_id"
348
- try:
349
- if hasattr(related_model, foreign_key_field):
350
- return {
351
- "type": "reverse_fk",
352
- "related_model": related_model,
353
- "foreign_key_field": foreign_key_field,
354
- "ref_field": "id",
355
- }
356
- except Exception: # noqa
357
- pass
358
-
359
307
  return None
360
308
 
361
309
  @staticmethod
362
310
  def _resolve_model_class(argument):
363
- """Resolve model class from string or class argument."""
364
311
  if isinstance(argument, str):
365
312
  from ...model import ObjectModel
366
313
 
367
- # Try to get any ObjectModel subclass to access the registry
368
314
  for subclass in ObjectModel.__subclasses__():
369
315
  if hasattr(subclass, "__registry__"):
370
316
  try:
@@ -372,13 +318,10 @@ class RelationshipAnalyzer:
372
318
  except Exception:
373
319
  continue
374
320
 
375
- # Fallback to recursive search if registry lookup fails
376
321
  def find_subclass(base_class):
377
- """Recursively find subclass by name."""
378
322
  for subclass in base_class.__subclasses__():
379
323
  if subclass.__name__ == argument:
380
324
  return subclass
381
- # Recursively search in subclasses
382
325
  found = find_subclass(subclass)
383
326
  if found:
384
327
  return found
sqlobjects/metadata.py CHANGED
@@ -215,7 +215,7 @@ class ModelRegistry(SqlAlchemyMetaData):
215
215
  if fk.column.table.name == target_table_name:
216
216
  property_.uselist = False
217
217
  if not property_.foreign_keys:
218
- property_.foreign_keys = col.name
218
+ property_.foreign_keys = [col.name]
219
219
  return
220
220
 
221
221
  # No FK found, assume one-to-many
sqlobjects/queryset.py CHANGED
@@ -181,6 +181,9 @@ class Q:
181
181
  _, field_name, value = expr
182
182
  field_column = table.c[field_name]
183
183
  conditions.append(field_column == value)
184
+ elif isinstance(expr, Q):
185
+ # Nested Q object passed as positional argument
186
+ conditions.append(expr._to_sqlalchemy(table))
184
187
  elif hasattr(expr, "resolve"):
185
188
  # Resolve SQLObjects expressions
186
189
  conditions.append(expr.resolve(table)) # type: ignore[reportAttributeAccessIssue]
sqlobjects/session.py CHANGED
@@ -62,7 +62,7 @@ class AsyncSession:
62
62
  # Return engine if no connection yet
63
63
  return get_database(self._db_name).engine
64
64
 
65
- async def execute(self, statement: Any, parameters: Any = None) -> CursorResult[Any]:
65
+ async def execute(self, statement: Any, parameters: Any = None) -> "CursorResult[Any]":
66
66
  """Execute statement with automatic transaction management.
67
67
 
68
68
  Supports both SQLAlchemy statement objects and raw SQL strings.
@@ -85,12 +85,21 @@ class AsyncSession:
85
85
  if self.auto_commit:
86
86
  await self.commit()
87
87
 
88
- return result
88
+ # Buffer rows before closing so the connection can be returned to the pool.
89
+ # Readonly sessions only run SELECT queries, so freeze()() is safe.
90
+ # Write sessions with auto_commit need rowcount/inserted_primary_key which
91
+ # are already captured in the CursorResult before close() is called.
92
+ if self.readonly:
93
+ result = result.freeze()()
94
+
95
+ return result # type: ignore[return-value]
89
96
  except Exception as e:
90
97
  await self._handle_exception(e)
98
+ raise # unreachable, _handle_exception always raises
91
99
  finally:
92
100
  # Auto-close connection for implicit sessions to prevent resource leaks
93
- if self.auto_commit:
101
+ # This covers both auto_commit write sessions and readonly sessions
102
+ if self.auto_commit or self.readonly:
94
103
  await self.close()
95
104
 
96
105
  async def stream(self, statement: Any, parameters: Any = None) -> AsyncResult[Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.2.3
3
+ Version: 1.2.4
4
4
  Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -2,11 +2,11 @@ sqlobjects/__init__.py,sha256=AiZiXBEjvrSItmhnlgyvMDCipU49XmvQPAujg-EoEDg,977
2
2
  sqlobjects/_install_rules.py,sha256=ndw9815-60WY_ORGUefijUBnoUHPd0NBzUkCL8mQLKc,4053
3
3
  sqlobjects/cascade.py,sha256=HLHB4TEoqK7m2i4X9JM0oX3nbltuTntaX3tMX1B1ym4,38389
4
4
  sqlobjects/exceptions.py,sha256=IEp5XV6N3hc3SAU-C3F-5D6jEZU_p72mufvwVhaStHo,16592
5
- sqlobjects/metadata.py,sha256=YNmeQqOTMSOD4Pqnmef1PS3P4wVRPvoW-TnnlCHNwEk,41479
5
+ sqlobjects/metadata.py,sha256=ftumbrSTJGN73jtg0e88vPwGURQAxRkho6B41YOMU-Q,41481
6
6
  sqlobjects/mixins.py,sha256=TBZFYAfa9RKF0UpxloBwREoWRPrWFHQE8MjOu7Aepu0,25501
7
7
  sqlobjects/model.py,sha256=PwP-0XhlVktICRCEye0gYlXrkVQG13yErjOwwf2ssJ0,19273
8
- sqlobjects/queryset.py,sha256=cjoEV-R2qitAIL_TuOx7ZpmEo_DzmNcIwee2zIMAFOI,49903
9
- sqlobjects/session.py,sha256=ODIxoGtTqdXDN9sa8eZ0ShxzwddjjuicUpv6pntZDM0,14282
8
+ sqlobjects/queryset.py,sha256=nIouDo4oL6a3fdrNNAeicFHeYYIx0fpdOpFND5wugAg,50079
9
+ sqlobjects/session.py,sha256=2XaAkboxcL8qHOcUHWc6wXUqiN1IzNoBRpozQLae8C0,14887
10
10
  sqlobjects/signals.py,sha256=N4mhotmMy_Ub-XJCvdlfsQ767KVzPbbJdF0JlJ405Oc,17454
11
11
  sqlobjects/validators.py,sha256=aqP7YgCnx-cjthqRo2RD-Iz2lvFsJsDgT73HwLwZGxE,10334
12
12
  sqlobjects/database/__init__.py,sha256=6lSRw4RzaWhomWEMyPWnpza7UxACQ6JhcFWxnLZZXlU,434
@@ -26,15 +26,15 @@ sqlobjects/expressions/window.py,sha256=0Olsi422n04SuemGjhIB_WwA9vTGJvMqOjHPWBFj
26
26
  sqlobjects/fields/__init__.py,sha256=SXYpmfS3l0CqKTIJsOD68wiQIE_x2ORYk2D7gb_E7Qc,1459
27
27
  sqlobjects/fields/core.py,sha256=IUGofZ-BlPuMzQBsqH25Ri9eGvLkenur7969u_wMniU,38712
28
28
  sqlobjects/fields/functions.py,sha256=KuCdtjTdfXo_1c2MNSlcPhyQmjPo8xFIt4CNUZnvU88,4281
29
- sqlobjects/fields/proxies.py,sha256=MS7z9p94QVU2xes2ryPfJns1lYGPiT5Dr9maAJJskTo,16800
29
+ sqlobjects/fields/proxies.py,sha256=nUSKYWDdmv91--0TzWh5-YK-fauW-piVVAiLZtMWjqM,17541
30
30
  sqlobjects/fields/shortcuts.py,sha256=euYyW0DliR77jLZDRQLNSsCpIHJBoRc7kzTBOIvHQFo,22794
31
31
  sqlobjects/fields/utils.py,sha256=HokUdlkyrUT85U468_ngWH7wbGj-VSjSet1AYG64tqI,3549
32
32
  sqlobjects/fields/relations/__init__.py,sha256=Bw64TIxt_HlRzRkjlu1t2hMRSvktWGxtI3BXdleTcVQ,629
33
- sqlobjects/fields/relations/descriptors.py,sha256=OZhW1GhF0YX17kSEx_ewKpN-Kdl_Pw4NcKB6mcksyNU,5647
33
+ sqlobjects/fields/relations/descriptors.py,sha256=Dws4L_GyF8WljU__jSDkgc559Syx6ERz8elj_vse15o,5222
34
34
  sqlobjects/fields/relations/managers.py,sha256=k0dU4xisRCvUTTTowEQpJUM1BXVt3J65lDInAC9W5zg,2919
35
- sqlobjects/fields/relations/prefetch.py,sha256=oX_cCmREFOY9-m3CO-rlMzKisFhP80GrQEVD7k8K2BE,10082
35
+ sqlobjects/fields/relations/prefetch.py,sha256=ccWO8hKfUUJVPoL5CPLUSMtscwT-XZg82hNArxh2nmc,6206
36
36
  sqlobjects/fields/relations/strategies.py,sha256=X4KkeBxwpCcBHWhcL9EGH2WTQ2NdZWBJzm3SMZagpIQ,566
37
- sqlobjects/fields/relations/utils.py,sha256=VDKT9MQ2WQ8jrS3Bw7K49S-py8Sfr4Mtc6l3BoeudlU,13975
37
+ sqlobjects/fields/relations/utils.py,sha256=G1PbW1heCShUSfjaDIbqj4I0yODdy4j3udf0Ex70cOg,12487
38
38
  sqlobjects/fields/types/__init__.py,sha256=PaEslOWuQVZVUoVnUFtqF-sNHOvF9AEVhuDseqRr-GA,651
39
39
  sqlobjects/fields/types/base.py,sha256=aK-8UDYtUcqJtwxw_emR-WefKDhrQyp0BK7s-UKx4yw,463
40
40
  sqlobjects/fields/types/comparators.py,sha256=d9azQGpUGg7eNF_aR0wWjZDzL3sFClHNh-fyObQudgY,15556
@@ -54,17 +54,17 @@ sqlobjects/utils/__init__.py,sha256=NsJ52Fl_PaB-2WlOIJVGNPmgOnqPmvYRmp7uaq5zfYs,
54
54
  sqlobjects/utils/inspect.py,sha256=u-Q9VBoSGYCE_3Soy8jMuCAdM-P7IkSzTxqrJop_Ilk,2612
55
55
  sqlobjects/utils/naming.py,sha256=PmfNuEz-Twly2qREjKfMNulJt8TmOUJaxzFTuts3COg,1411
56
56
  sqlobjects/utils/pattern.py,sha256=FGCgobTpnmKjdAtQidHIg4Xa2CQLeP35F0VuX_1T1dk,14740
57
- sqlobjects-1.2.3.data/data/share/sqlobjects/rules/01-database-session-guide.md,sha256=lnqRpcDhYKdH-f0avW1jXcWt3qi7XBwkMt7clTs9Zvg,7601
58
- sqlobjects-1.2.3.data/data/share/sqlobjects/rules/02-model-definition-guide.md,sha256=i5gNJM-0bIJ2xSUu3mij8uB9kJC9EAc_439sCBqRph8,10977
59
- sqlobjects-1.2.3.data/data/share/sqlobjects/rules/03-query-operations-guide.md,sha256=BEZE_WUUhY6sEZDiDtrbInlV_UVIhbYETABJTNhu2JQ,10550
60
- sqlobjects-1.2.3.data/data/share/sqlobjects/rules/04-crud-operations-guide.md,sha256=abUEvRPSiSjX8enclXO5Uryox9Rt0WFeRcFkvHXfPf8,10085
61
- sqlobjects-1.2.3.data/data/share/sqlobjects/rules/05-relationships-guide.md,sha256=MJ23fYOTWqrLd54ENZLu3S3QRqbLf7joq-YhF6-V8OQ,12204
62
- sqlobjects-1.2.3.data/data/share/sqlobjects/rules/06-validation-signals-guide.md,sha256=i9WLLhyCDoWGkIYqH2D_XLn98oMbF1pB3-L-b5gRbT4,14294
63
- sqlobjects-1.2.3.data/data/share/sqlobjects/rules/07-performance-guide.md,sha256=rjR0DjT39poEBVaUxAzJBEaMaBrZKLVer1zXaNNYfxg,12851
64
- sqlobjects-1.2.3.data/data/share/sqlobjects/rules/README.md,sha256=d2n5ZZT8LGvjL60u_HlzXn2uA8FwoCgqG3fuAhvMFvM,2280
65
- sqlobjects-1.2.3.dist-info/licenses/LICENSE,sha256=ScFR7nvWIhar0d32Y-LA4vqp9zNmy1HSJia7UX7jU6c,1068
66
- sqlobjects-1.2.3.dist-info/METADATA,sha256=La4cgkP-taTiemMfbkPbwQknox7UwK-1pwoycrjQpaI,13740
67
- sqlobjects-1.2.3.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
68
- sqlobjects-1.2.3.dist-info/entry_points.txt,sha256=9Q3Ci55MawxkQhMlR_eLJui00_rYYIVO_Aq_Dm0u5MQ,76
69
- sqlobjects-1.2.3.dist-info/top_level.txt,sha256=brO5vDtCpn4L9dpX0EHpehbu4kcHaK_2Bbnwze35YKc,11
70
- sqlobjects-1.2.3.dist-info/RECORD,,
57
+ sqlobjects-1.2.4.data/data/share/sqlobjects/rules/01-database-session-guide.md,sha256=lnqRpcDhYKdH-f0avW1jXcWt3qi7XBwkMt7clTs9Zvg,7601
58
+ sqlobjects-1.2.4.data/data/share/sqlobjects/rules/02-model-definition-guide.md,sha256=i5gNJM-0bIJ2xSUu3mij8uB9kJC9EAc_439sCBqRph8,10977
59
+ sqlobjects-1.2.4.data/data/share/sqlobjects/rules/03-query-operations-guide.md,sha256=BEZE_WUUhY6sEZDiDtrbInlV_UVIhbYETABJTNhu2JQ,10550
60
+ sqlobjects-1.2.4.data/data/share/sqlobjects/rules/04-crud-operations-guide.md,sha256=abUEvRPSiSjX8enclXO5Uryox9Rt0WFeRcFkvHXfPf8,10085
61
+ sqlobjects-1.2.4.data/data/share/sqlobjects/rules/05-relationships-guide.md,sha256=MJ23fYOTWqrLd54ENZLu3S3QRqbLf7joq-YhF6-V8OQ,12204
62
+ sqlobjects-1.2.4.data/data/share/sqlobjects/rules/06-validation-signals-guide.md,sha256=i9WLLhyCDoWGkIYqH2D_XLn98oMbF1pB3-L-b5gRbT4,14294
63
+ sqlobjects-1.2.4.data/data/share/sqlobjects/rules/07-performance-guide.md,sha256=rjR0DjT39poEBVaUxAzJBEaMaBrZKLVer1zXaNNYfxg,12851
64
+ sqlobjects-1.2.4.data/data/share/sqlobjects/rules/README.md,sha256=d2n5ZZT8LGvjL60u_HlzXn2uA8FwoCgqG3fuAhvMFvM,2280
65
+ sqlobjects-1.2.4.dist-info/licenses/LICENSE,sha256=ScFR7nvWIhar0d32Y-LA4vqp9zNmy1HSJia7UX7jU6c,1068
66
+ sqlobjects-1.2.4.dist-info/METADATA,sha256=Wql2U7dUxjC3mK9nGp7iF7YoVpHJBbynF798QO4Kc0c,13740
67
+ sqlobjects-1.2.4.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
68
+ sqlobjects-1.2.4.dist-info/entry_points.txt,sha256=9Q3Ci55MawxkQhMlR_eLJui00_rYYIVO_Aq_Dm0u5MQ,76
69
+ sqlobjects-1.2.4.dist-info/top_level.txt,sha256=brO5vDtCpn4L9dpX0EHpehbu4kcHaK_2Bbnwze35YKc,11
70
+ sqlobjects-1.2.4.dist-info/RECORD,,