TypeDAL 3.8.4__tar.gz → 3.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of TypeDAL might be problematic. Click here for more details.

Files changed (58) hide show
  1. {typedal-3.8.4 → typedal-3.9.0}/CHANGELOG.md +13 -0
  2. {typedal-3.8.4 → typedal-3.9.0}/PKG-INFO +1 -1
  3. {typedal-3.8.4 → typedal-3.9.0}/example_new.py +3 -1
  4. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/__about__.py +1 -1
  5. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/core.py +61 -15
  6. {typedal-3.8.4 → typedal-3.9.0}/tests/test_query_builder.py +15 -0
  7. {typedal-3.8.4 → typedal-3.9.0}/tests/test_relationships.py +90 -6
  8. {typedal-3.8.4 → typedal-3.9.0}/.github/workflows/su6.yml +0 -0
  9. {typedal-3.8.4 → typedal-3.9.0}/.gitignore +0 -0
  10. {typedal-3.8.4 → typedal-3.9.0}/.readthedocs.yml +0 -0
  11. {typedal-3.8.4 → typedal-3.9.0}/README.md +0 -0
  12. {typedal-3.8.4 → typedal-3.9.0}/coverage.svg +0 -0
  13. {typedal-3.8.4 → typedal-3.9.0}/docs/1_getting_started.md +0 -0
  14. {typedal-3.8.4 → typedal-3.9.0}/docs/2_defining_tables.md +0 -0
  15. {typedal-3.8.4 → typedal-3.9.0}/docs/3_building_queries.md +0 -0
  16. {typedal-3.8.4 → typedal-3.9.0}/docs/4_relationships.md +0 -0
  17. {typedal-3.8.4 → typedal-3.9.0}/docs/5_py4web.md +0 -0
  18. {typedal-3.8.4 → typedal-3.9.0}/docs/6_migrations.md +0 -0
  19. {typedal-3.8.4 → typedal-3.9.0}/docs/7_mixins.md +0 -0
  20. {typedal-3.8.4 → typedal-3.9.0}/docs/css/code_blocks.css +0 -0
  21. {typedal-3.8.4 → typedal-3.9.0}/docs/index.md +0 -0
  22. {typedal-3.8.4 → typedal-3.9.0}/docs/requirements.txt +0 -0
  23. {typedal-3.8.4 → typedal-3.9.0}/example_old.py +0 -0
  24. {typedal-3.8.4 → typedal-3.9.0}/mkdocs.yml +0 -0
  25. {typedal-3.8.4 → typedal-3.9.0}/pyproject.toml +0 -0
  26. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/__init__.py +0 -0
  27. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/caching.py +0 -0
  28. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/cli.py +0 -0
  29. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/config.py +0 -0
  30. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/fields.py +0 -0
  31. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/for_py4web.py +0 -0
  32. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/for_web2py.py +0 -0
  33. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/helpers.py +0 -0
  34. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/mixins.py +0 -0
  35. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/py.typed +0 -0
  36. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/serializers/as_json.py +0 -0
  37. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/types.py +0 -0
  38. {typedal-3.8.4 → typedal-3.9.0}/src/typedal/web2py_py4web_shared.py +0 -0
  39. {typedal-3.8.4 → typedal-3.9.0}/tests/__init__.py +0 -0
  40. {typedal-3.8.4 → typedal-3.9.0}/tests/configs/simple.toml +0 -0
  41. {typedal-3.8.4 → typedal-3.9.0}/tests/configs/valid.env +0 -0
  42. {typedal-3.8.4 → typedal-3.9.0}/tests/configs/valid.toml +0 -0
  43. {typedal-3.8.4 → typedal-3.9.0}/tests/test_cli.py +0 -0
  44. {typedal-3.8.4 → typedal-3.9.0}/tests/test_config.py +0 -0
  45. {typedal-3.8.4 → typedal-3.9.0}/tests/test_docs_examples.py +0 -0
  46. {typedal-3.8.4 → typedal-3.9.0}/tests/test_helpers.py +0 -0
  47. {typedal-3.8.4 → typedal-3.9.0}/tests/test_json.py +0 -0
  48. {typedal-3.8.4 → typedal-3.9.0}/tests/test_main.py +0 -0
  49. {typedal-3.8.4 → typedal-3.9.0}/tests/test_mixins.py +0 -0
  50. {typedal-3.8.4 → typedal-3.9.0}/tests/test_mypy.py +0 -0
  51. {typedal-3.8.4 → typedal-3.9.0}/tests/test_orm.py +0 -0
  52. {typedal-3.8.4 → typedal-3.9.0}/tests/test_py4web.py +0 -0
  53. {typedal-3.8.4 → typedal-3.9.0}/tests/test_row.py +0 -0
  54. {typedal-3.8.4 → typedal-3.9.0}/tests/test_stats.py +0 -0
  55. {typedal-3.8.4 → typedal-3.9.0}/tests/test_table.py +0 -0
  56. {typedal-3.8.4 → typedal-3.9.0}/tests/test_web2py.py +0 -0
  57. {typedal-3.8.4 → typedal-3.9.0}/tests/test_xx_others.py +0 -0
  58. {typedal-3.8.4 → typedal-3.9.0}/tests/timings.py +0 -0
@@ -2,6 +2,19 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.9.0 (2024-10-25)
6
+
7
+ ### Feature
8
+
9
+ * Add `condition_and` to .join so you can add additional requirements to inner joins ([`0ae688b`](https://github.com/trialandsuccess/TypeDAL/commit/0ae688ba01f421c70a660b5bb5d9672484494aa4))
10
+
11
+ ## v3.8.5 (2024-10-24)
12
+
13
+ ### Fix
14
+
15
+ * Use right 'timestamp' field ([`2aedb02`](https://github.com/trialandsuccess/TypeDAL/commit/2aedb027b4490f8284253e9c7f427b0660b6bc13))
16
+ * Allow specifying a field to Builder.count(...); support selecting extra fields (e.g. MyField.count()) ([`ce28a79`](https://github.com/trialandsuccess/TypeDAL/commit/ce28a7995a6d817424462f8b18383c85fa349ba4))
17
+
5
18
  ## v3.8.4 (2024-10-24)
6
19
 
7
20
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 3.8.4
3
+ Version: 3.9.0
4
4
  Summary: Typing support for PyDAL
5
5
  Project-URL: Documentation, https://typedal.readthedocs.io/
6
6
  Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
@@ -8,6 +8,8 @@ from src.typedal.fields import TextField
8
8
  from src.typedal.helpers import utcnow
9
9
  from pydal.validators import IS_NOT_EMPTY
10
10
 
11
+ from typedal.fields import TimestampField
12
+
11
13
  db = TypeDAL("sqlite:memory")
12
14
 
13
15
 
@@ -20,7 +22,7 @@ class Person(TypedTable):
20
22
  age = TypedField(int, default=18, requires=IS_NOT_EMPTY())
21
23
  nicknames: list[str]
22
24
 
23
- ts = TypedField(dt.datetime, type="timestamp")
25
+ ts = TimestampField()
24
26
 
25
27
 
26
28
  assert db.person._format == "%(name)s"
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "3.8.4"
8
+ __version__ = "3.9.0"
@@ -130,6 +130,7 @@ class Relationship(typing.Generic[To_Type]):
130
130
  _type: To_Type
131
131
  table: Type["TypedTable"] | type | str
132
132
  condition: Condition
133
+ condition_and: Condition
133
134
  on: OnQuery
134
135
  multiple: bool
135
136
  join: JOIN_OPTIONS
@@ -140,6 +141,7 @@ class Relationship(typing.Generic[To_Type]):
140
141
  condition: Condition = None,
141
142
  join: JOIN_OPTIONS = None,
142
143
  on: OnQuery = None,
144
+ condition_and: Condition = None,
143
145
  ):
144
146
  """
145
147
  Should not be called directly, use relationship() instead!
@@ -152,6 +154,7 @@ class Relationship(typing.Generic[To_Type]):
152
154
  self.condition = condition
153
155
  self.join = "left" if on else join # .on is always left join!
154
156
  self.on = on
157
+ self.condition_and = condition_and
155
158
 
156
159
  if args := typing.get_args(_type):
157
160
  self.table = unwrap_type(args[0])
@@ -172,6 +175,7 @@ class Relationship(typing.Generic[To_Type]):
172
175
  update.get("condition") or self.condition,
173
176
  update.get("join") or self.join,
174
177
  update.get("on") or self.on,
178
+ update.get("condition_and") or self.condition_and,
175
179
  )
176
180
 
177
181
  def __repr__(self) -> str:
@@ -180,6 +184,10 @@ class Relationship(typing.Generic[To_Type]):
180
184
  """
181
185
  if callback := self.condition or self.on:
182
186
  src_code = inspect.getsource(callback).strip()
187
+
188
+ if c_and := self.condition_and:
189
+ and_code = inspect.getsource(c_and).strip()
190
+ src_code += " AND " + and_code
183
191
  else:
184
192
  cls_name = self._type if isinstance(self._type, str) else self._type.__name__ # type: ignore
185
193
  src_code = f"to {cls_name} (missing condition)"
@@ -1050,11 +1058,12 @@ class TableMeta(type):
1050
1058
  method: JOIN_OPTIONS = None,
1051
1059
  on: OnQuery | list[Expression] | Expression = None,
1052
1060
  condition: Condition = None,
1061
+ condition_and: Condition = None,
1053
1062
  ) -> "QueryBuilder[T_MetaInstance]":
1054
1063
  """
1055
1064
  See QueryBuilder.join!
1056
1065
  """
1057
- return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method)
1066
+ return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method, condition_and=condition_and)
1058
1067
 
1059
1068
  def collect(self: Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
1060
1069
  """
@@ -1500,7 +1509,14 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1500
1509
  return None # type: ignore
1501
1510
 
1502
1511
  inst._row = row
1503
- inst.__dict__.update(row)
1512
+
1513
+ if hasattr(row, "id"):
1514
+ inst.__dict__.update(row)
1515
+ else:
1516
+ # deal with _extra (and possibly others?)
1517
+ # Row <{actual: {}, _extra: ...}>
1518
+ inst.__dict__.update(row[str(cls)])
1519
+
1504
1520
  inst._setup_instance_methods()
1505
1521
  return inst
1506
1522
 
@@ -1828,7 +1844,22 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
1828
1844
  `metadata` can be any (un)structured data
1829
1845
  `model` is a Typed Table class
1830
1846
  """
1831
- records = records or {row.id: model(row) for row in rows}
1847
+
1848
+ def _get_id(row: Row) -> int:
1849
+ """
1850
+ Try to find the id field in a row.
1851
+
1852
+ If _extra exists, the row changes:
1853
+ <Row {'test_relationship': {'id': 1}, '_extra': {'COUNT("test_relationship"."querytable")': 8}}>
1854
+ """
1855
+ if idx := getattr(row, "id", None):
1856
+ return typing.cast(int, idx)
1857
+ elif main := getattr(row, str(model), None):
1858
+ return typing.cast(int, main.id)
1859
+ else: # pragma: no cover
1860
+ raise NotImplementedError(f"`id` could not be found for {row}")
1861
+
1862
+ records = records or {_get_id(row): model(row) for row in rows}
1832
1863
  super().__init__(rows.db, records, rows.colnames, rows.compact, rows.response, rows.fields)
1833
1864
  self.model = model
1834
1865
  self.metadata = metadata or {}
@@ -2295,6 +2326,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2295
2326
  method: JOIN_OPTIONS = None,
2296
2327
  on: OnQuery | list[Expression] | Expression = None,
2297
2328
  condition: Condition = None,
2329
+ condition_and: Condition = None,
2298
2330
  ) -> "QueryBuilder[T_MetaInstance]":
2299
2331
  """
2300
2332
  Include relationship fields in the result.
@@ -2304,8 +2336,13 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2304
2336
 
2305
2337
  By default, the `method` defined in the relationship is used.
2306
2338
  This can be overwritten with the `method` keyword argument (left or inner)
2339
+
2340
+ `condition_and` can be used to add extra conditions to an inner join.
2307
2341
  """
2308
2342
  # todo: allow limiting amount of related rows returned for join?
2343
+ # todo: it would be nice if 'fields' could be an actual relationship
2344
+ # (Article.tags = list[Tag]) and you could change the .condition and .on
2345
+ # this could deprecate condition_and
2309
2346
 
2310
2347
  relationships = self.model.get_relationships()
2311
2348
 
@@ -2318,7 +2355,9 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2318
2355
  if isinstance(condition, pydal.objects.Query):
2319
2356
  condition = as_lambda(condition)
2320
2357
 
2321
- relationships = {str(fields[0]): Relationship(fields[0], condition=condition, join=method)}
2358
+ relationships = {
2359
+ str(fields[0]): Relationship(fields[0], condition=condition, join=method, condition_and=condition_and)
2360
+ }
2322
2361
  elif on:
2323
2362
  if len(fields) != 1:
2324
2363
  raise ValueError("join(field, on=...) can only be used with exactly one field!")
@@ -2328,15 +2367,17 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2328
2367
 
2329
2368
  if isinstance(on, list):
2330
2369
  on = as_lambda(on)
2331
- relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method)}
2370
+ relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method, condition_and=condition_and)}
2332
2371
 
2333
2372
  else:
2334
2373
  if fields:
2335
2374
  # join on every relationship
2336
- relationships = {str(k): relationships[str(k)] for k in fields}
2375
+ relationships = {str(k): relationships[str(k)].clone(condition_and=condition_and) for k in fields}
2337
2376
 
2338
2377
  if method:
2339
- relationships = {str(k): r.clone(join=method) for k, r in relationships.items()}
2378
+ relationships = {
2379
+ str(k): r.clone(join=method, condition_and=condition_and) for k, r in relationships.items()
2380
+ }
2340
2381
 
2341
2382
  return self._extend(relationships=relationships)
2342
2383
 
@@ -2557,7 +2598,6 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2557
2598
 
2558
2599
  metadata["relationships"] = set(self.relationships.keys())
2559
2600
 
2560
- # query = self._update_query_for_inner(db, model, query)
2561
2601
  join = []
2562
2602
  for key, relation in self.relationships.items():
2563
2603
  if not relation.condition or relation.join != "inner":
@@ -2565,7 +2605,11 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2565
2605
 
2566
2606
  other = relation.get_table(db)
2567
2607
  other = other.with_alias(f"{key}_{hash(relation)}")
2568
- join.append(other.on(relation.condition(model, other)))
2608
+ condition = relation.condition(model, other)
2609
+ if callable(relation.condition_and):
2610
+ condition &= relation.condition_and(model, other)
2611
+
2612
+ join.append(other.on(condition))
2569
2613
 
2570
2614
  if limitby := select_kwargs.pop("limitby", ()):
2571
2615
 
@@ -2615,12 +2659,12 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2615
2659
  # .on not given, generate it:
2616
2660
  other = other.with_alias(f"{key}_{hash(relation)}")
2617
2661
  condition = typing.cast(Query, relation.condition(model, other))
2662
+ if callable(relation.condition_and):
2663
+ condition &= relation.condition_and(model, other)
2618
2664
  left.append(other.on(condition))
2619
2665
  else:
2620
2666
  # else: inner join (handled earlier)
2621
2667
  other = other.with_alias(f"{key}_{hash(relation)}") # only for replace
2622
- # other = other.with_alias(f"{key}_{hash(relation)}")
2623
- # query &= relation.condition(model, other)
2624
2668
 
2625
2669
  # if no fields of 'other' are included, add other.ALL
2626
2670
  # else: only add other.id if missing
@@ -2721,7 +2765,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2721
2765
  """
2722
2766
  yield from self.collect()
2723
2767
 
2724
- def count(self) -> int:
2768
+ def count(self, distinct: bool = None) -> int:
2725
2769
  """
2726
2770
  Return the amount of rows matching the current query.
2727
2771
  """
@@ -2730,14 +2774,16 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2730
2774
  query = self.query
2731
2775
 
2732
2776
  for key, relation in self.relationships.items():
2733
- if not relation.condition or relation.join != "inner":
2777
+ if (not relation.condition or relation.join != "inner") and not distinct:
2734
2778
  continue
2735
2779
 
2736
2780
  other = relation.get_table(db)
2737
- other = other.with_alias(f"{key}_{hash(relation)}")
2781
+ if not distinct:
2782
+ # todo: can this lead to other issues?
2783
+ other = other.with_alias(f"{key}_{hash(relation)}")
2738
2784
  query &= relation.condition(model, other)
2739
2785
 
2740
- return db(query).count()
2786
+ return db(query).count(distinct)
2741
2787
 
2742
2788
  def __paginate(
2743
2789
  self,
@@ -400,6 +400,7 @@ def test_complex_join():
400
400
 
401
401
 
402
402
  def test_reprs_and_bool():
403
+ _setup_data()
403
404
  assert TestQueryTable.where(id=1)
404
405
  assert not TestQueryTable.where(id=101)
405
406
 
@@ -471,3 +472,17 @@ def test_column():
471
472
 
472
473
  assert len(rows) == 4
473
474
  assert set(rows) == {33}
475
+
476
+
477
+ def test_collect_with_extra_fields():
478
+ _setup_data()
479
+ builder = TestRelationship.select(TestRelationship.id, TestRelationship.name, TestRelationship.querytable.count())
480
+
481
+ assert builder.execute()
482
+
483
+ row = builder.first_or_fail()
484
+
485
+ assert row.id
486
+ assert row.name
487
+ assert row._extra
488
+ assert row[TestRelationship.querytable.count()]
@@ -43,7 +43,7 @@ class Role(TypedTable, TaggableMixin):
43
43
  @db.define()
44
44
  class User(TypedTable, TaggableMixin):
45
45
  gid = TypedField(str, default=uuid4)
46
- name: str
46
+ name: TypedField[str]
47
47
  roles: TypedField[list[Role]]
48
48
  main_role = TypedField(Role)
49
49
  extra_roles = TypedField(list[Role])
@@ -110,6 +110,11 @@ def _setup_data():
110
110
  ]
111
111
  )
112
112
 
113
+ # no relationships:
114
+ new_author = User.insert(name="Untagged Author", roles=[], main_role=writer, extra_roles=[])
115
+ untagged1 = Article.insert(title="Untagged Article 1", author=new_author)
116
+ untagged2 = Article.insert(title="Untagged Article 2", author=new_author)
117
+
113
118
  # articles
114
119
 
115
120
  article1, article2 = Article.bulk_insert(
@@ -184,11 +189,11 @@ def test_typedal_way():
184
189
 
185
190
  all_articles = Article.join().collect().as_dict()
186
191
 
187
- assert all_articles[1]["final_editor"]["name"] == "Editor 1"
188
- assert all_articles[2]["secondary_author"]["name"] == "Editor 1"
192
+ assert all_articles[3]["final_editor"]["name"] == "Editor 1"
193
+ assert all_articles[4]["secondary_author"]["name"] == "Editor 1"
189
194
 
190
- assert all_articles[1]["secondary_author"] is None
191
- assert all_articles[2]["final_editor"] is None
195
+ assert all_articles[3]["secondary_author"] is None
196
+ assert all_articles[4]["final_editor"] is None
192
197
 
193
198
  assert Article.first_or_fail()
194
199
 
@@ -244,7 +249,7 @@ def test_typedal_way():
244
249
 
245
250
  users = User.join().collect()
246
251
 
247
- assert len(users) == 3 # reader, writer, editor
252
+ assert len(users) == 4 # reader, writer, editor, untagged
248
253
 
249
254
  # get by id:
250
255
  reader = users[1]
@@ -290,6 +295,7 @@ def test_typedal_way():
290
295
 
291
296
 
292
297
  def test_reprs():
298
+ _setup_data()
293
299
  assert "Relationship:left on=" in repr(Article.tags)
294
300
 
295
301
  article = Article.first()
@@ -318,6 +324,16 @@ def test_reprs():
318
324
 
319
325
  assert empty.get_table_name() == "new"
320
326
 
327
+ relation = Article.join("author").relationships["author"]
328
+
329
+ assert "AND" not in repr(relation)
330
+
331
+ relation = Article.join("author",
332
+ condition_and=lambda article, author: author.name != "Hank"
333
+ ).relationships["author"]
334
+
335
+ assert "AND" in repr(relation) and "Hank" in repr(relation)
336
+
321
337
 
322
338
  @db.define()
323
339
  class CacheFirst(TypedTable):
@@ -350,6 +366,33 @@ def test_relationship_detection():
350
366
  assert user_table_relationships["extra_roles"].join == "left"
351
367
 
352
368
 
369
+ def test_join_with_different_condition():
370
+ _setup_data()
371
+
372
+ role_with_users = Role.join(
373
+ "users",
374
+ method="inner",
375
+ ).first()
376
+
377
+ assert role_with_users.users
378
+ assert role_with_users.users[0].name == "Reader 1"
379
+
380
+ role_with_users = Role.join(
381
+ "users", method="inner", condition_and=lambda role, user: ~user.name.like("Reader%")
382
+ ).first()
383
+
384
+ assert role_with_users.users
385
+ assert role_with_users.users[0].name != "Reader 1"
386
+
387
+ # left:
388
+ role_with_users = Role.join(
389
+ "users", method="left", condition_and=lambda role, user: ~user.name.like("Reader%")
390
+ ).first()
391
+
392
+ assert role_with_users.users
393
+ assert role_with_users.users[0].name != "Reader 1"
394
+
395
+
353
396
  def test_caching():
354
397
  uncached = User.join().collect_or_fail()
355
398
  cached = User.cache().join().collect_or_fail() # not actually cached yet!
@@ -536,3 +579,44 @@ def test_illegal():
536
579
 
537
580
  class HasRelationship:
538
581
  something = relationship("...", condition=lambda: 1, on=lambda: 2)
582
+
583
+
584
+ def test_join_with_select():
585
+ _setup_data()
586
+
587
+ builder = User.select(User.id, User.gid, Article.id, Article.gid).where(id=2).join("articles")
588
+ user = builder.first_or_fail()
589
+
590
+ assert user.id
591
+ assert user.gid
592
+ assert not user.name
593
+ assert user.articles[0].id
594
+ assert user.articles[0].gid
595
+ assert not hasattr(user.articles[0], "title")
596
+
597
+ for user in builder.paginate(limit=1, page=1):
598
+ assert user.id
599
+ assert user.gid
600
+ assert not user.name
601
+ assert user.articles[0].id
602
+ assert user.articles[0].gid
603
+ assert not hasattr(user.articles[0], "title")
604
+
605
+ for user in builder.collect():
606
+ assert user.id
607
+ assert user.gid
608
+ assert not user.name
609
+ assert user.articles[0].id
610
+ assert user.articles[0].gid
611
+ assert not hasattr(user.articles[0], "title")
612
+
613
+
614
+ def test_count_with_join():
615
+ _setup_data()
616
+
617
+ # 0. count via select:
618
+ row = User.select(User.id, User.gid, Article.id, Article.gid).where(id=4).join("articles").first_or_fail()
619
+ assert len(row.articles) == 2
620
+
621
+ assert User.where(id=4).join("articles").count(User.id) == 1
622
+ assert User.where(id=4).join("articles").count(Article.id) == 2
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
File without changes
File without changes
File without changes
File without changes
File without changes