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.
- {typedal-3.8.4 → typedal-3.9.0}/CHANGELOG.md +13 -0
- {typedal-3.8.4 → typedal-3.9.0}/PKG-INFO +1 -1
- {typedal-3.8.4 → typedal-3.9.0}/example_new.py +3 -1
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/__about__.py +1 -1
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/core.py +61 -15
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_query_builder.py +15 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_relationships.py +90 -6
- {typedal-3.8.4 → typedal-3.9.0}/.github/workflows/su6.yml +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/.gitignore +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/.readthedocs.yml +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/README.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/coverage.svg +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/1_getting_started.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/2_defining_tables.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/3_building_queries.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/4_relationships.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/5_py4web.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/6_migrations.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/7_mixins.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/css/code_blocks.css +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/index.md +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/docs/requirements.txt +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/example_old.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/mkdocs.yml +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/pyproject.toml +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/__init__.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/caching.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/cli.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/config.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/fields.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/for_py4web.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/for_web2py.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/helpers.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/mixins.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/py.typed +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/serializers/as_json.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/types.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/__init__.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/configs/simple.toml +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/configs/valid.env +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/configs/valid.toml +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_cli.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_config.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_docs_examples.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_helpers.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_json.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_main.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_mixins.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_mypy.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_orm.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_py4web.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_row.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_stats.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_table.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_web2py.py +0 -0
- {typedal-3.8.4 → typedal-3.9.0}/tests/test_xx_others.py +0 -0
- {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
|
|
@@ -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 =
|
|
25
|
+
ts = TimestampField()
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
assert db.person._format == "%(name)s"
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
188
|
-
assert all_articles[
|
|
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[
|
|
191
|
-
assert all_articles[
|
|
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) ==
|
|
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
|
|
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
|