sqlalchemy-boltons 1.0.1__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.0.1 → sqlalchemy_boltons-2.0.0}/CHANGELOG.md +6 -0
  2. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/PKG-INFO +34 -1
  3. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/README.md +33 -0
  4. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/pyproject.toml +1 -1
  5. sqlalchemy_boltons-2.0.0/src/sqlalchemy_boltons/orm.py +74 -0
  6. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons/sqlite.py +15 -0
  7. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons.egg-info/PKG-INFO +34 -1
  8. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons.egg-info/SOURCES.txt +2 -0
  9. sqlalchemy_boltons-2.0.0/tests/test_orm.py +83 -0
  10. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/tests/test_sqlite.py +9 -1
  11. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/AUTHORS.md +0 -0
  12. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/LICENSE +0 -0
  13. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/MANIFEST.in +0 -0
  14. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/doc/Makefile +0 -0
  15. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/doc/conf.py +0 -0
  16. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/doc/index.rst +0 -0
  17. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/doc/make.bat +0 -0
  18. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/setup.cfg +0 -0
  19. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/setup.py +0 -0
  20. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons/__init__.py +0 -0
  21. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons/py.typed +0 -0
  22. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons.egg-info/dependency_links.txt +0 -0
  23. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/src/sqlalchemy_boltons.egg-info/top_level.txt +0 -0
  24. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/tests/__init__.py +0 -0
  25. {sqlalchemy_boltons-1.0.1 → sqlalchemy_boltons-2.0.0}/tests/conftest.py +0 -0
@@ -4,6 +4,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.1.0] - 2025-03-11
8
+
9
+ ### Changed
10
+
11
+ - `sqlite`: Use an unlimited-size QueuePool by default.
12
+
7
13
  ## [1.0.1] - 2025-02-22
8
14
 
9
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlalchemy_boltons
3
- Version: 1.0.1
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.0.1"
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
@@ -54,6 +56,16 @@ class SQLAlchemySqliteTransactionFix:
54
56
  This class exists because sqlalchemy doesn't automatically fix pysqlite's stupid default behaviour. Additionally,
55
57
  we implement support for foreign keys.
56
58
 
59
+ The execution options we look for are as follows:
60
+
61
+ - `x_sqlite_begin_mode`: The type of transaction to be started, such as "BEGIN" or
62
+ "[BEGIN IMMEDIATE](https://www.sqlite.org/lang_transaction.html)" (or
63
+ "[BEGIN CONCURRENT](https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begin_concurrent.md)" someday maybe).
64
+ - `x_sqlite_foreign_keys`: The [foreign-key enforcement setting](https://www.sqlite.org/foreignkeys.html). Must be
65
+ `True`, `False`, or `"defer"`.
66
+ - `x_sqlite_journal_mode`: The [journal mode](https://www.sqlite.org/pragma.html#pragma_journal_mode) such as
67
+ `"DELETE"` or `"WAL"`. Optional.
68
+
57
69
  https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#serializable-isolation-savepoints-transactional-ddl
58
70
  """
59
71
 
@@ -188,6 +200,9 @@ def create_engine_sqlite(
188
200
  # always default to QueuePool
189
201
  if (k := "poolclass") not in create_engine_args:
190
202
  create_engine_args[k] = sap.QueuePool
203
+ if (k := "max_overflow") not in create_engine_args:
204
+ # for SQLite it doesn't make sense to restrict the number of concurrent (read-only) connections
205
+ create_engine_args["max_overflow"] = -1
191
206
 
192
207
  if (v := create_engine_args.get(k := "connect_args")) is None:
193
208
  create_engine_args[k] = v = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlalchemy-boltons
3
- Version: 1.0.1
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))
@@ -54,7 +54,9 @@ def simple_sqlite_engine(tmp_path, database_type):
54
54
  else:
55
55
  raise AssertionError
56
56
 
57
- return _sq.create_engine_sqlite(path, journal_mode="WAL", timeout=0.5, create_engine_args={"echo": True})
57
+ return _sq.create_engine_sqlite(
58
+ path, journal_mode="WAL", timeout=0.5, create_engine_args={"echo": True, "pool_timeout": 2}
59
+ )
58
60
 
59
61
 
60
62
  @pytest.mark.parametrize("database_type", ["file", "memory"])
@@ -117,6 +119,12 @@ def test_transaction(simple_sqlite_engine, database_type):
117
119
  assert len(s.execute(sa.select(Example)).all()) == 2
118
120
  s.commit()
119
121
 
122
+ with contextlib.ExitStack() as exit_stack:
123
+ # concurrent connection count
124
+ for i in range(30):
125
+ exit_stack.enter_context(s := SessionR())
126
+ assert len(s.execute(sa.select(Example)).all()) == 2
127
+
120
128
 
121
129
  @pytest.mark.parametrize("path_type", ["str", "Path"])
122
130
  def test_create_engine_path(tmp_path, path_type):