TypeDAL 4.0.1__tar.gz → 4.0.2__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-4.0.1 → typedal-4.0.2}/CHANGELOG.md +6 -0
- {typedal-4.0.1 → typedal-4.0.2}/PKG-INFO +1 -1
- {typedal-4.0.1 → typedal-4.0.2}/pyproject.toml +2 -2
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/__about__.py +1 -1
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/__init__.py +1 -1
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/cli.py +1 -2
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/query_builder.py +1 -1
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_helpers.py +2 -1
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_relationships.py +129 -22
- {typedal-4.0.1 → typedal-4.0.2}/.github/workflows/su6.yml +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/.gitignore +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/.readthedocs.yml +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/README.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/coverage.svg +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/1_getting_started.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/2_defining_tables.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/3_building_queries.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/4_relationships.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/5_py4web.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/6_migrations.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/7_mixins.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/css/code_blocks.css +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/index.md +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/docs/requirements.txt +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/example_new.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/example_old.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/mkdocs.yml +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/caching.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/config.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/constants.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/core.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/define.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/fields.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/for_py4web.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/for_web2py.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/helpers.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/mixins.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/py.typed +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/relationships.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/rows.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/tables.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/types.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/__init__.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/configs/simple.toml +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/configs/valid.env +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/configs/valid.toml +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/py314_tests.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_cli.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_config.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_docs_examples.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_json.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_main.py +1 -1
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_mixins.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_mypy.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_orm.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_py4web.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_query_builder.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_row.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_stats.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_table.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_web2py.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/test_xx_others.py +0 -0
- {typedal-4.0.1 → typedal-4.0.2}/tests/timings.py +0 -0
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.0.2 (2025-10-14)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* Resolve nested join bug where shared foreign key relationships weren't loaded correctly ([`b093454`](https://github.com/trialandsuccess/TypeDAL/commit/b09345410576cf0a61c0395d91b03d60d2c056a2))
|
|
10
|
+
|
|
5
11
|
## v4.0.1 (2025-10-13)
|
|
6
12
|
|
|
7
13
|
### Fix
|
|
@@ -118,7 +118,7 @@ badge = true
|
|
|
118
118
|
mypy = "--disable-error-code misc"
|
|
119
119
|
|
|
120
120
|
[tool.black]
|
|
121
|
-
target-version = ["
|
|
121
|
+
target-version = ["py314"]
|
|
122
122
|
line-length = 120
|
|
123
123
|
# 'extend-exclude' excludes files or directories in addition to the defaults
|
|
124
124
|
extend-exclude = '''
|
|
@@ -161,7 +161,7 @@ strict = true
|
|
|
161
161
|
exclude = ["venv", ".bak"]
|
|
162
162
|
|
|
163
163
|
[tool.ruff]
|
|
164
|
-
target-version = "
|
|
164
|
+
target-version = "py314"
|
|
165
165
|
line-length = 120
|
|
166
166
|
|
|
167
167
|
extend-exclude = ["*.bak/", "venv*/"]
|
|
@@ -7,7 +7,7 @@ from .fields import TypedField
|
|
|
7
7
|
from .helpers import sql_expression
|
|
8
8
|
from .query_builder import QueryBuilder
|
|
9
9
|
from .relationships import Relationship, relationship
|
|
10
|
-
from .rows import
|
|
10
|
+
from .rows import PaginatedRows, TypedRows
|
|
11
11
|
from .tables import TypedTable
|
|
12
12
|
|
|
13
13
|
from . import fields # isort: skip
|
|
@@ -392,8 +392,7 @@ def fake_migrations(
|
|
|
392
392
|
|
|
393
393
|
previously_migrated = (
|
|
394
394
|
db(
|
|
395
|
-
db.ewh_implemented_features.name.belongs(to_fake)
|
|
396
|
-
& (db.ewh_implemented_features.installed == True) # noqa E712
|
|
395
|
+
db.ewh_implemented_features.name.belongs(to_fake) & (db.ewh_implemented_features.installed == True) # noqa E712
|
|
397
396
|
)
|
|
398
397
|
.select(db.ewh_implemented_features.name)
|
|
399
398
|
.column("name")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import datetime as dt
|
|
1
2
|
import sys
|
|
2
3
|
import typing
|
|
3
4
|
|
|
@@ -27,7 +28,6 @@ from src.typedal.helpers import (
|
|
|
27
28
|
)
|
|
28
29
|
from src.typedal.types import Field
|
|
29
30
|
|
|
30
|
-
import datetime as dt
|
|
31
31
|
|
|
32
32
|
def test_is_union():
|
|
33
33
|
assert is_union(int | str)
|
|
@@ -267,6 +267,7 @@ def test_sql_expression():
|
|
|
267
267
|
assert str(database.sql_expression("LOWER(%s)", TestSqlExpression.value)) == 'LOWER("test_sql_expression"."value")'
|
|
268
268
|
assert str(database.sql_expression("LOWER(%s.value)", TestSqlExpression)) == 'LOWER("test_sql_expression".value)'
|
|
269
269
|
|
|
270
|
+
|
|
270
271
|
@pytest.mark.skipif(not SYSTEM_SUPPORTS_TEMPLATES, reason="t-strings contain breaking syntax!")
|
|
271
272
|
def test_sql_expression_314():
|
|
272
273
|
from .py314_tests import test_sql_expression_314
|
|
@@ -17,9 +17,6 @@ from src.typedal.caching import (
|
|
|
17
17
|
db = TypeDAL("sqlite:memory")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
# db = TypeDAL("sqlite://debug.db")
|
|
21
|
-
|
|
22
|
-
|
|
23
20
|
class TaggableMixin:
|
|
24
21
|
tags = relationship(
|
|
25
22
|
list["Tag"],
|
|
@@ -87,8 +84,7 @@ class Tagged(TypedTable): # pivot table
|
|
|
87
84
|
|
|
88
85
|
|
|
89
86
|
@db.define()
|
|
90
|
-
class Empty(TypedTable):
|
|
91
|
-
...
|
|
87
|
+
class Empty(TypedTable): ...
|
|
92
88
|
|
|
93
89
|
|
|
94
90
|
def _setup_data():
|
|
@@ -297,8 +293,7 @@ def test_typedal_way():
|
|
|
297
293
|
author1 = User.where(id=4).join().first()
|
|
298
294
|
|
|
299
295
|
assert (
|
|
300
|
-
|
|
301
|
-
dict(author1)["articles"]) == 2
|
|
296
|
+
len(author1.as_dict()["articles"]) == len(author1.__dict__["articles"]) == len(dict(author1)["articles"]) == 2
|
|
302
297
|
)
|
|
303
298
|
|
|
304
299
|
|
|
@@ -386,7 +381,9 @@ def test_join_with_different_condition():
|
|
|
386
381
|
assert role_with_users.users[0].name == "Reader 1"
|
|
387
382
|
|
|
388
383
|
role_with_users = Role.join(
|
|
389
|
-
"users",
|
|
384
|
+
"users",
|
|
385
|
+
method="inner",
|
|
386
|
+
condition_and=lambda role, user: ~user.name.like("Reader%"),
|
|
390
387
|
).first()
|
|
391
388
|
|
|
392
389
|
assert role_with_users.users
|
|
@@ -394,7 +391,9 @@ def test_join_with_different_condition():
|
|
|
394
391
|
|
|
395
392
|
# left:
|
|
396
393
|
role_with_users = Role.join(
|
|
397
|
-
"users",
|
|
394
|
+
"users",
|
|
395
|
+
method="left",
|
|
396
|
+
condition_and=lambda role, user: ~user.name.like("Reader%"),
|
|
398
397
|
).first()
|
|
399
398
|
|
|
400
399
|
assert role_with_users.users
|
|
@@ -431,12 +430,12 @@ def test_caching():
|
|
|
431
430
|
cached_user_only2 = User.join().cache(User.id).collect_or_fail()
|
|
432
431
|
|
|
433
432
|
assert (
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
433
|
+
len(uncached2)
|
|
434
|
+
== len(uncached)
|
|
435
|
+
== len(cached2)
|
|
436
|
+
== len(cached)
|
|
437
|
+
== len(cached_user_only2)
|
|
438
|
+
== len(cached_user_only)
|
|
440
439
|
)
|
|
441
440
|
|
|
442
441
|
assert uncached.as_json() == uncached2.as_json() == cached.as_json() == cached2.as_json()
|
|
@@ -444,9 +443,9 @@ def test_caching():
|
|
|
444
443
|
assert cached.first().gid == cached2.first().gid
|
|
445
444
|
|
|
446
445
|
assert (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
446
|
+
[_.name for _ in uncached2.first().roles]
|
|
447
|
+
== [_.name for _ in cached.first().roles]
|
|
448
|
+
== [_.name for _ in cached2.first().roles]
|
|
450
449
|
)
|
|
451
450
|
|
|
452
451
|
assert not uncached2.metadata.get("cache", {}).get("enabled")
|
|
@@ -584,6 +583,7 @@ def test_caching_dependencies():
|
|
|
584
583
|
|
|
585
584
|
def test_illegal():
|
|
586
585
|
with pytest.raises(ValueError), pytest.warns(UserWarning):
|
|
586
|
+
|
|
587
587
|
class HasRelationship:
|
|
588
588
|
something = relationship("...", condition=lambda: 1, on=lambda: 2)
|
|
589
589
|
|
|
@@ -667,9 +667,10 @@ def test_nested_relationships():
|
|
|
667
667
|
|
|
668
668
|
assert old_besties == new_besties == {"Editor 1": "-", "Reader 1": "Reader's Bestie", "Writer 1": "-"}
|
|
669
669
|
|
|
670
|
-
|
|
671
670
|
# more complex:
|
|
672
|
-
role = Role.where(name="reader").join(
|
|
671
|
+
role = Role.where(name="reader").join(
|
|
672
|
+
"users.bestie", "users.articles.final_editor", "users.articles.secondary_author"
|
|
673
|
+
)
|
|
673
674
|
|
|
674
675
|
nested_article = role.first().users[2].articles[0]
|
|
675
676
|
|
|
@@ -679,7 +680,113 @@ def test_nested_relationships():
|
|
|
679
680
|
assert not nested_article.final_editor
|
|
680
681
|
|
|
681
682
|
# complex, inner:
|
|
682
|
-
role_inner = Role.where(name="reader").join(
|
|
683
|
+
role_inner = Role.where(name="reader").join(
|
|
684
|
+
"users.bestie", "users.articles.final_editor", "users.articles.secondary_author", method="inner"
|
|
685
|
+
)
|
|
683
686
|
|
|
684
687
|
# no final_editor -> inner join should fail:
|
|
685
|
-
assert not role_inner.first()
|
|
688
|
+
assert not role_inner.first()
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
"""
|
|
692
|
+
In production I noticed this bug:
|
|
693
|
+
|
|
694
|
+
When joining nested data like `process = Process.join("pros_and_cons.product")`
|
|
695
|
+
process.pros_and_cons[0].product # is fine
|
|
696
|
+
process.pros_and_cons[1].product # is None
|
|
697
|
+
while they both share the same `product_gid`
|
|
698
|
+
This indicates that something goes wrong when collecting the nested data.
|
|
699
|
+
`pros_and_cons` is very specific to that production use-case so please think of a more general example and write a test for it.
|
|
700
|
+
1. define the required tables and relationships
|
|
701
|
+
2. define the required data
|
|
702
|
+
3. perform the query and check the results
|
|
703
|
+
"""
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class City(TypedTable):
|
|
707
|
+
gid = TypedField(str, default=uuid4)
|
|
708
|
+
name: str
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
class Office(TypedTable):
|
|
712
|
+
gid = TypedField(str, default=uuid4)
|
|
713
|
+
address: str
|
|
714
|
+
city_id: City
|
|
715
|
+
company: "Company"
|
|
716
|
+
|
|
717
|
+
city_alternative = relationship(City, lambda office, city: office.city_id == city.id)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
class Company(TypedTable):
|
|
721
|
+
name: str
|
|
722
|
+
|
|
723
|
+
offices = relationship(list[Office], lambda self, other: other.company == self.id)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def test_nested_join_with_shared_foreign_key():
|
|
727
|
+
"""
|
|
728
|
+
Test that nested joins properly load relationships for all items,
|
|
729
|
+
especially when multiple items share the same foreign key value.
|
|
730
|
+
|
|
731
|
+
Bug: When joining nested data like company.offices[0].city works
|
|
732
|
+
but company.offices[1].city is None even though both offices
|
|
733
|
+
reference the same city.
|
|
734
|
+
"""
|
|
735
|
+
db.define(City)
|
|
736
|
+
db.define(Company)
|
|
737
|
+
db.define(Office)
|
|
738
|
+
|
|
739
|
+
# Create a city
|
|
740
|
+
san_francisco = City.insert(name="San Francisco")
|
|
741
|
+
|
|
742
|
+
# Create a company
|
|
743
|
+
tech_corp = Company.insert(name="Tech Corp")
|
|
744
|
+
|
|
745
|
+
# Create multiple offices in the same city for the same company
|
|
746
|
+
office1 = Office.insert(address="123 Market St", city_id=san_francisco.id, company=tech_corp.id)
|
|
747
|
+
office2 = Office.insert(address="456 Mission St", city_id=san_francisco.id, company=tech_corp.id)
|
|
748
|
+
office3 = Office.insert(address="789 Howard St", city_id=san_francisco.id, company=tech_corp.id)
|
|
749
|
+
|
|
750
|
+
db.commit()
|
|
751
|
+
|
|
752
|
+
# 1: direct Reference (not a Relationship)
|
|
753
|
+
|
|
754
|
+
# Query company with nested join to offices and their cities
|
|
755
|
+
company = Company.where(id=tech_corp.id).join("offices.city_id").first_or_fail()
|
|
756
|
+
|
|
757
|
+
# All offices should be loaded
|
|
758
|
+
assert len(company.offices) == 3, f"Expected 3 offices, got {len(company.offices)}"
|
|
759
|
+
|
|
760
|
+
# BUG TEST: All offices should have their city relationship loaded
|
|
761
|
+
# Not just the first one
|
|
762
|
+
for idx, office in enumerate(company.offices, 1):
|
|
763
|
+
assert office.city_id is not None, f"Office {idx} ('{office.address}'): city relationship is None (BUG!)"
|
|
764
|
+
assert isinstance(office.city_id, City), (
|
|
765
|
+
f"Office {idx}: city ({office.city_id}) is not a City instance but a {type(office.city_id)}"
|
|
766
|
+
)
|
|
767
|
+
assert office.city_id.name == "San Francisco", (
|
|
768
|
+
f"Office {idx}: expected 'San Francisco', got '{office.city_id.name}'"
|
|
769
|
+
)
|
|
770
|
+
assert office.city_id.id == san_francisco.id, f"Office {idx}: city id mismatch"
|
|
771
|
+
|
|
772
|
+
# 2. alternative: city_alternative (Relationship)
|
|
773
|
+
|
|
774
|
+
# Query company with nested join to offices and their cities
|
|
775
|
+
company = Company.where(id=tech_corp.id).join("offices.city_alternative").first_or_fail()
|
|
776
|
+
|
|
777
|
+
# All offices should be loaded
|
|
778
|
+
assert len(company.offices) == 3, f"Expected 3 offices, got {len(company.offices)}"
|
|
779
|
+
|
|
780
|
+
# BUG TEST: All offices should have their city relationship loaded
|
|
781
|
+
# Not just the first one
|
|
782
|
+
for idx, office in enumerate(company.offices, 1):
|
|
783
|
+
assert office.city_alternative is not None, (
|
|
784
|
+
f"Office {idx} ('{office.address}'): city relationship is None (BUG!)"
|
|
785
|
+
)
|
|
786
|
+
assert isinstance(office.city_alternative, City), (
|
|
787
|
+
f"Office {idx}: city ({office.city_alternative}) is not a City instance but a {type(office.city_alternative)}"
|
|
788
|
+
)
|
|
789
|
+
assert office.city_alternative.name == "San Francisco", (
|
|
790
|
+
f"Office {idx}: expected 'San Francisco', got '{office.city_alternative.name}'"
|
|
791
|
+
)
|
|
792
|
+
assert office.city_alternative.id == san_francisco.id, f"Office {idx}: city id mismatch"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|