sqlalchemy-boltons 1.1.0__tar.gz → 2.0.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.
Files changed (25) hide show
  1. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/PKG-INFO +34 -1
  2. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/README.md +33 -0
  3. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/pyproject.toml +1 -1
  4. sqlalchemy_boltons-2.0.0/src/sqlalchemy_boltons/orm.py +74 -0
  5. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons/sqlite.py +2 -0
  6. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons.egg-info/PKG-INFO +34 -1
  7. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons.egg-info/SOURCES.txt +2 -0
  8. sqlalchemy_boltons-2.0.0/tests/test_orm.py +83 -0
  9. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/AUTHORS.md +0 -0
  10. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/CHANGELOG.md +0 -0
  11. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/LICENSE +0 -0
  12. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/MANIFEST.in +0 -0
  13. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/doc/Makefile +0 -0
  14. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/doc/conf.py +0 -0
  15. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/doc/index.rst +0 -0
  16. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/doc/make.bat +0 -0
  17. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/setup.cfg +0 -0
  18. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/setup.py +0 -0
  19. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons/__init__.py +0 -0
  20. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons/py.typed +0 -0
  21. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons.egg-info/dependency_links.txt +0 -0
  22. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons.egg-info/top_level.txt +0 -0
  23. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/tests/__init__.py +0 -0
  24. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/tests/conftest.py +0 -0
  25. {sqlalchemy_boltons-1.1.0 → sqlalchemy_boltons-2.0.0}/tests/test_sqlite.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlalchemy_boltons
3
- Version: 1.1.0
3
+ Version: 2.0.0
4
4
  Summary: Utilities that should've been inside SQLAlchemy but aren't
5
5
  Author-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
6
6
  Maintainer-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
@@ -71,3 +71,36 @@ with Session() as session:
71
71
  with SessionW() as session:
72
72
  session.execute(update(...))
73
73
  ```
74
+
75
+ ## orm
76
+
77
+ Somedays you really wish you could write something like:
78
+
79
+ ```python
80
+ # Find every parent that has at least one child.
81
+ Parent1 = aliased(Parent)
82
+ select(Parent1).where(
83
+ exists().select_from(Child).where(Child.parent == Parent1) # this doesn't work
84
+ )
85
+ ```
86
+
87
+ But it doesn't work. You get an exception. You try various other things but it just won't produce the right subquery.
88
+ You find tons of people online saying you must resign yourself to writing the explicit filtering condition, writing out
89
+ `.where(Child.parent_id == Parent1.id)`. Like a caveman. Why even bother using an ORM at this point? If you're lucky,
90
+ you find yourself reading mail archives from [a decade ago](https://groups.google.com/g/sqlalchemy/c/R-qOlzzVi0o/m/NtFswgJioDIJ)
91
+ with somewhat of a solution, but it's not obvious how to make it work with table
92
+ [aliases](https://docs.sqlalchemy.org/en/20/orm/queryguide/api.html#sqlalchemy.orm.aliased). Despair is setting in.
93
+
94
+ But lucky you, you can now use this library:
95
+
96
+ ```python
97
+ from sqlalchemy_boltons.orm import RelationshipComparator as Rel
98
+
99
+ # Find every parent that has at least one child.
100
+ Parent1 = aliased(Parent)
101
+ select(Parent1).where(
102
+ exists().select_from(Child).where(Rel(Child.parent) == Parent1)
103
+ ) # ^^^^ ^
104
+ ```
105
+
106
+ Hope is restored.
@@ -57,3 +57,36 @@ with Session() as session:
57
57
  with SessionW() as session:
58
58
  session.execute(update(...))
59
59
  ```
60
+
61
+ ## orm
62
+
63
+ Somedays you really wish you could write something like:
64
+
65
+ ```python
66
+ # Find every parent that has at least one child.
67
+ Parent1 = aliased(Parent)
68
+ select(Parent1).where(
69
+ exists().select_from(Child).where(Child.parent == Parent1) # this doesn't work
70
+ )
71
+ ```
72
+
73
+ But it doesn't work. You get an exception. You try various other things but it just won't produce the right subquery.
74
+ You find tons of people online saying you must resign yourself to writing the explicit filtering condition, writing out
75
+ `.where(Child.parent_id == Parent1.id)`. Like a caveman. Why even bother using an ORM at this point? If you're lucky,
76
+ you find yourself reading mail archives from [a decade ago](https://groups.google.com/g/sqlalchemy/c/R-qOlzzVi0o/m/NtFswgJioDIJ)
77
+ with somewhat of a solution, but it's not obvious how to make it work with table
78
+ [aliases](https://docs.sqlalchemy.org/en/20/orm/queryguide/api.html#sqlalchemy.orm.aliased). Despair is setting in.
79
+
80
+ But lucky you, you can now use this library:
81
+
82
+ ```python
83
+ from sqlalchemy_boltons.orm import RelationshipComparator as Rel
84
+
85
+ # Find every parent that has at least one child.
86
+ Parent1 = aliased(Parent)
87
+ select(Parent1).where(
88
+ exists().select_from(Child).where(Rel(Child.parent) == Parent1)
89
+ ) # ^^^^ ^
90
+ ```
91
+
92
+ Hope is restored.
@@ -3,7 +3,7 @@ line-length = 120
3
3
 
4
4
  [project]
5
5
  name = "sqlalchemy_boltons"
6
- version = "1.1.0"
6
+ version = "2.0.0"
7
7
  description = "Utilities that should've been inside SQLAlchemy but aren't"
8
8
  readme = "README.md"
9
9
  license = { text = "MIT" }
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy.sql import coercions as _coercions, util as _util
4
+
5
+
6
+ class RelationshipComparator:
7
+ """
8
+ This wrapper makes it possible to compare a table against a relationship using :func:`join_expr`. This can be used
9
+ for constructing a subquery that filters using a relationship to a table from the outer query.
10
+
11
+ For example::
12
+
13
+ import sqlalchemy as sa
14
+ from sqlalchemy import orm as sao
15
+ from sqlalchemy_boltons.orm import RelationshipComparator as Rel
16
+
17
+ Base = sao.declarative_base()
18
+
19
+
20
+ class Parent(Base):
21
+ __tablename__ = "parent"
22
+
23
+ id = sa.Column(sa.Integer, primary_key=True)
24
+
25
+ children = sao.relationship("Child", back_populates="parent")
26
+
27
+
28
+ class Child(Base):
29
+ __tablename__ = "child"
30
+
31
+ id = sa.Column(sa.Integer, primary_key=True)
32
+ parent_id = sa.Column(sa.ForeignKey("parent.id"), index=True)
33
+
34
+ parent = sao.relationship("Parent", back_populates="children")
35
+
36
+
37
+ # We want a query that selects every Parent that has at least one child. We also want to use aliases.
38
+ P = sao.aliased(Parent)
39
+ C = sao.aliased(Child)
40
+
41
+ # This is the boring old way of doing it, which requires explicitly stating the filtering conditions in terms
42
+ # of the columns in both tables.
43
+ q1 = sa.select(P).where(sa.select(C).where(C.parent_id == P.id).exists())
44
+
45
+ # Reuse the filtering conditions from the ORM relationship!
46
+ q2 = sa.select(P).where(sa.select(C).where(Rel(C.parent) == P).exists())
47
+
48
+ assert str(q1) == str(q2), "these should produce the same SQL code!"
49
+
50
+ Based on this discussion: https://groups.google.com/g/sqlalchemy/c/R-qOlzzVi0o/m/NtFswgJioDIJ
51
+ """
52
+
53
+ def __init__(self, relationship):
54
+ self._relationship = relationship
55
+
56
+ def __eq__(self, other):
57
+ if other is None:
58
+ raise TypeError("cannot compare against None")
59
+ return join_expr(other, self._relationship)
60
+
61
+ def __ne__(self, other):
62
+ return ~(self == other)
63
+
64
+
65
+ def join_expr(right, relationship):
66
+ """
67
+ Turn an ORM relationship into an expression that you can use for filtering.
68
+ """
69
+
70
+ expr = _coercions.expect(_coercions.roles.ColumnArgumentRole, relationship)
71
+ if right is not None:
72
+ right = _coercions.expect(_coercions.roles.FromClauseRole, right)
73
+ expr = _util.ClauseAdapter(right).traverse(expr)
74
+ return expr
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses as dc
2
4
  import functools
3
5
  from pathlib import Path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlalchemy-boltons
3
- Version: 1.1.0
3
+ Version: 2.0.0
4
4
  Summary: Utilities that should've been inside SQLAlchemy but aren't
5
5
  Author-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
6
6
  Maintainer-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
@@ -71,3 +71,36 @@ with Session() as session:
71
71
  with SessionW() as session:
72
72
  session.execute(update(...))
73
73
  ```
74
+
75
+ ## orm
76
+
77
+ Somedays you really wish you could write something like:
78
+
79
+ ```python
80
+ # Find every parent that has at least one child.
81
+ Parent1 = aliased(Parent)
82
+ select(Parent1).where(
83
+ exists().select_from(Child).where(Child.parent == Parent1) # this doesn't work
84
+ )
85
+ ```
86
+
87
+ But it doesn't work. You get an exception. You try various other things but it just won't produce the right subquery.
88
+ You find tons of people online saying you must resign yourself to writing the explicit filtering condition, writing out
89
+ `.where(Child.parent_id == Parent1.id)`. Like a caveman. Why even bother using an ORM at this point? If you're lucky,
90
+ you find yourself reading mail archives from [a decade ago](https://groups.google.com/g/sqlalchemy/c/R-qOlzzVi0o/m/NtFswgJioDIJ)
91
+ with somewhat of a solution, but it's not obvious how to make it work with table
92
+ [aliases](https://docs.sqlalchemy.org/en/20/orm/queryguide/api.html#sqlalchemy.orm.aliased). Despair is setting in.
93
+
94
+ But lucky you, you can now use this library:
95
+
96
+ ```python
97
+ from sqlalchemy_boltons.orm import RelationshipComparator as Rel
98
+
99
+ # Find every parent that has at least one child.
100
+ Parent1 = aliased(Parent)
101
+ select(Parent1).where(
102
+ exists().select_from(Child).where(Rel(Child.parent) == Parent1)
103
+ ) # ^^^^ ^
104
+ ```
105
+
106
+ Hope is restored.
@@ -11,6 +11,7 @@ doc/conf.py
11
11
  doc/index.rst
12
12
  doc/make.bat
13
13
  src/sqlalchemy_boltons/__init__.py
14
+ src/sqlalchemy_boltons/orm.py
14
15
  src/sqlalchemy_boltons/py.typed
15
16
  src/sqlalchemy_boltons/sqlite.py
16
17
  src/sqlalchemy_boltons.egg-info/PKG-INFO
@@ -19,4 +20,5 @@ src/sqlalchemy_boltons.egg-info/dependency_links.txt
19
20
  src/sqlalchemy_boltons.egg-info/top_level.txt
20
21
  tests/__init__.py
21
22
  tests/conftest.py
23
+ tests/test_orm.py
22
24
  tests/test_sqlite.py
@@ -0,0 +1,83 @@
1
+ import sqlalchemy as sa
2
+ import sqlalchemy.orm as sao
3
+
4
+ from sqlalchemy_boltons import orm as _orm
5
+
6
+ import pytest
7
+
8
+
9
+ Base = sao.declarative_base()
10
+
11
+
12
+ def _make_rel_1(p=None, c=None):
13
+ p = p or Parent1
14
+ c = c or Child1
15
+ return p.id == c.parent_id
16
+
17
+
18
+ def _make_rel_2(p=None, c=None):
19
+ p = p or Parent2
20
+ c = c or Child2
21
+ return (p.ix == c.parent_ix) & (p.iy == c.parent_iy)
22
+
23
+
24
+ class Parent1(Base):
25
+ __tablename__ = "parent1"
26
+
27
+ id = sa.Column(sa.Integer, primary_key=True)
28
+
29
+ children = sao.relationship("Child1", back_populates="parent")
30
+
31
+
32
+ class Child1(Base):
33
+ __tablename__ = "child1"
34
+
35
+ id = sa.Column(sa.Integer, primary_key=True)
36
+ parent_id = sa.Column(sa.ForeignKey("parent1.id"))
37
+
38
+ parent = sao.relationship("Parent1", back_populates="children")
39
+
40
+
41
+ class Parent2(Base):
42
+ __tablename__ = "parent2"
43
+
44
+ ix = sa.Column(sa.Integer, primary_key=True)
45
+ iy = sa.Column(sa.String, primary_key=True)
46
+
47
+ children = sao.relationship("Child2", back_populates="parent", primaryjoin=_make_rel_2)
48
+
49
+
50
+ class Child2(Base):
51
+ __tablename__ = "child2"
52
+
53
+ id = sa.Column(sa.Integer, primary_key=True)
54
+ parent_ix = sa.Column(sa.ForeignKey("parent2.ix"))
55
+ parent_iy = sa.Column(sa.ForeignKey("parent2.iy"))
56
+
57
+ parent = sao.relationship("Parent2", primaryjoin=_make_rel_2)
58
+
59
+
60
+ _cases = {"1": (Parent1, Child1, _make_rel_1), "2": (Parent2, Child2, _make_rel_2)}
61
+
62
+
63
+ def assert_same_sql(x, y):
64
+ assert str(x) == str(y), "expected equivalent SQL"
65
+
66
+
67
+ @pytest.mark.parametrize("case", _cases.keys())
68
+ @pytest.mark.parametrize("alias_parent", [False, True])
69
+ @pytest.mark.parametrize("alias_child", [False, True])
70
+ def test_join_expr(case, alias_parent, alias_child):
71
+ Parent, Child, make_rel = _cases[case]
72
+
73
+ P = sao.aliased(Parent) if alias_parent else Parent
74
+ C = sao.aliased(Child) if alias_child else Child
75
+
76
+ assert_same_sql(_orm.join_expr(P, C.parent), make_rel(P, C))
77
+
78
+ Rel = _orm.RelationshipComparator
79
+
80
+ if alias_parent:
81
+ assert_same_sql(Rel(C.parent) == P, make_rel(P, C))
82
+ else:
83
+ assert_same_sql(_orm.join_expr(None, C.parent), make_rel(P, C))