sqlqb 0.1.2__tar.gz → 0.1.3__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.
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlqb
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A simple sql query builder
5
5
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlqb"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "A simple sql query builder"
5
5
  readme = "README.md"
6
6
  authors = []
@@ -13,6 +13,12 @@ dev = [
13
13
  "pytest-cov>=7.1.0",
14
14
  "ruff>=0.15.17",
15
15
  ]
16
+ pymysql = [
17
+ "pymysql>=1.2.0",
18
+ ]
19
+ sqlalchemy = [
20
+ "sqlalchemy>=2.0.51",
21
+ ]
16
22
 
17
23
  [build-system]
18
24
  requires = ["hatchling", "hatch-vcs"]
@@ -8,7 +8,7 @@ from sqlqb import Delete as _Delete
8
8
 
9
9
 
10
10
  class Select(_Select):
11
- def __init__(self, connection, *args):
11
+ def __init__(self, connection: _Connection, *args):
12
12
  super().__init__(*args)
13
13
  self.__connection = connection
14
14
 
@@ -31,7 +31,7 @@ class Select(_Select):
31
31
 
32
32
 
33
33
  class Insert(_Insert):
34
- def __init__(self, connection):
34
+ def __init__(self, connection: _Connection):
35
35
  super().__init__()
36
36
  self.__connection = connection
37
37
 
@@ -42,7 +42,7 @@ class Insert(_Insert):
42
42
 
43
43
 
44
44
  class Update(_Update):
45
- def __init__(self, connection, table: str):
45
+ def __init__(self, connection: _Connection, table: str):
46
46
  super().__init__(table)
47
47
  self.__connection = connection
48
48
 
@@ -53,7 +53,7 @@ class Update(_Update):
53
53
 
54
54
 
55
55
  class Delete(_Delete):
56
- def __init__(self, connection):
56
+ def __init__(self, connection: _Connection):
57
57
  super().__init__()
58
58
  self.__connection = connection
59
59
 
@@ -75,8 +75,11 @@ class Connection(_Connection):
75
75
  def Insert(self) -> Insert:
76
76
  return Insert(self)
77
77
 
78
+ def Update(self, table: str) -> Update:
79
+ return Update(self, table)
80
+
78
81
  def Delete(self) -> Delete:
79
82
  return Delete(self)
80
83
 
81
- def Update(self, table: str) -> Update:
82
- return Update(self, table)
84
+
85
+ connect = Connection
@@ -0,0 +1,95 @@
1
+ from sqlalchemy import create_engine as _create_engine
2
+ from sqlalchemy import text
3
+ from sqlalchemy.engine import Connection as _Connection
4
+ from sqlalchemy.engine import Engine as _Engine
5
+
6
+ from sqlqb import Select as _Select
7
+ from sqlqb import Insert as _Insert
8
+ from sqlqb import Update as _Update
9
+ from sqlqb import Delete as _Delete
10
+
11
+
12
+ def _bind(sql: str, params: list) -> tuple[str, dict]:
13
+ """Replace ? placeholders with :p0, :p1, ... for SQLAlchemy text()."""
14
+ named: dict = {}
15
+ for i, val in enumerate(params):
16
+ key = f"p{i}"
17
+ sql = sql.replace("?", f":{key}", 1)
18
+ named[key] = val
19
+ return sql, named
20
+
21
+
22
+ class Select(_Select):
23
+ def __init__(self, connection: _Connection, *args):
24
+ super().__init__(*args)
25
+ self.__connection = connection
26
+
27
+ def execute(self):
28
+ sql, params = _bind(self.sql, self.params)
29
+ return self.__connection.execute(text(sql), params)
30
+
31
+ def fetchone(self):
32
+ sql, params = _bind(self.sql, self.params)
33
+ row = self.execute().mappings().fetchone()
34
+ return dict(row) if row is not None else None
35
+
36
+ def fetchall(self) -> list:
37
+ sql, params = _bind(self.sql, self.params)
38
+ return [dict(row) for row in self.execute().mappings()]
39
+
40
+
41
+ class Insert(_Insert):
42
+ def __init__(self, connection: _Connection):
43
+ super().__init__()
44
+ self.__connection = connection
45
+
46
+ def execute(self) -> int:
47
+ sql, params = _bind(self.sql, self.params)
48
+ return self.__connection.execute(text(sql), params).rowcount
49
+
50
+
51
+ class Update(_Update):
52
+ def __init__(self, connection: _Connection, table: str):
53
+ super().__init__(table)
54
+ self.__connection = connection
55
+
56
+ def execute(self) -> int:
57
+ sql, params = _bind(self.sql, self.params)
58
+ return self.__connection.execute(text(sql), params).rowcount
59
+
60
+
61
+ class Delete(_Delete):
62
+ def __init__(self, connection: _Connection):
63
+ super().__init__()
64
+ self.__connection = connection
65
+
66
+ def execute(self) -> int:
67
+ sql, params = _bind(self.sql, self.params)
68
+ return self.__connection.execute(text(sql), params).rowcount
69
+
70
+
71
+ class Connection(_Connection):
72
+ def Select(self, *args) -> Select:
73
+ return Select(self, *args)
74
+
75
+ def Insert(self) -> Insert:
76
+ return Insert(self)
77
+
78
+ def Update(self, table: str) -> Update:
79
+ return Update(self, table)
80
+
81
+ def Delete(self) -> Delete:
82
+ return Delete(self)
83
+
84
+
85
+ class Engine(_Engine):
86
+ def connect(self) -> Connection:
87
+ conn = super().connect()
88
+ conn.__class__ = Connection
89
+ return conn
90
+
91
+
92
+ def create_engine(url: str, **kwargs) -> Engine:
93
+ engine = _create_engine(url, **kwargs)
94
+ engine.__class__ = Engine
95
+ return engine
@@ -1,4 +1,6 @@
1
- import sqlite3
1
+ from sqlite3 import Connection as _Connection
2
+ from sqlite3 import Cursor as _Cursor
3
+ from sqlite3 import connect as _connect
2
4
 
3
5
  from sqlqb import Select as _Select
4
6
  from sqlqb import Insert as _Insert
@@ -7,11 +9,11 @@ from sqlqb import Delete as _Delete
7
9
 
8
10
 
9
11
  class Select(_Select):
10
- def __init__(self, connection, *args):
12
+ def __init__(self, connection: _Connection, *args):
11
13
  super().__init__(*args)
12
14
  self.__connection = connection
13
15
 
14
- def execute(self) -> sqlite3.Cursor:
16
+ def execute(self) -> _Cursor:
15
17
  return self.__connection.execute(self.sql, self.params)
16
18
 
17
19
  def fetchone(self):
@@ -22,7 +24,7 @@ class Select(_Select):
22
24
 
23
25
 
24
26
  class Insert(_Insert):
25
- def __init__(self, connection):
27
+ def __init__(self, connection: _Connection):
26
28
  super().__init__()
27
29
  self.__connection = connection
28
30
 
@@ -31,7 +33,7 @@ class Insert(_Insert):
31
33
 
32
34
 
33
35
  class Update(_Update):
34
- def __init__(self, connection, table: str):
36
+ def __init__(self, connection: _Connection, table: str):
35
37
  super().__init__(table)
36
38
  self.__connection = connection
37
39
 
@@ -40,7 +42,7 @@ class Update(_Update):
40
42
 
41
43
 
42
44
  class Delete(_Delete):
43
- def __init__(self, connection):
45
+ def __init__(self, connection: _Connection):
44
46
  super().__init__()
45
47
  self.__connection = connection
46
48
 
@@ -48,7 +50,7 @@ class Delete(_Delete):
48
50
  return self.__connection.execute(self.sql, self.params).rowcount
49
51
 
50
52
 
51
- class Connection(sqlite3.Connection):
53
+ class Connection(_Connection):
52
54
  def __init__(self, *args, **kwargs):
53
55
  super().__init__(*args, **kwargs)
54
56
  self.row_factory = lambda cursor, row: dict(zip([col[0] for col in cursor.description], row))
@@ -59,12 +61,12 @@ class Connection(sqlite3.Connection):
59
61
  def Insert(self) -> Insert:
60
62
  return Insert(self)
61
63
 
62
- def Delete(self) -> Delete:
63
- return Delete(self)
64
-
65
64
  def Update(self, table: str) -> Update:
66
65
  return Update(self, table)
67
66
 
67
+ def Delete(self) -> Delete:
68
+ return Delete(self)
69
+
68
70
 
69
71
  def connect(database, **kwargs) -> Connection:
70
- return sqlite3.connect(database, factory=Connection, **kwargs)
72
+ return _connect(database, factory=Connection, **kwargs)
@@ -0,0 +1,223 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ from sqlqb import pymysql
5
+ from sqlqb.pymysql import Select, Insert, Update, Delete, Connection
6
+ from pymysql.cursors import DictCursor
7
+
8
+
9
+ @pytest.fixture
10
+ def mock_conn():
11
+ """Mock pymysql connection whose cursor supports both direct use and context manager."""
12
+ conn = MagicMock()
13
+ cursor = MagicMock()
14
+ conn.cursor.return_value = cursor
15
+ cursor.__enter__.return_value = cursor
16
+ cursor.__exit__.return_value = False
17
+ return conn, cursor
18
+
19
+
20
+ @pytest.fixture
21
+ def connection():
22
+ """pymysql.Connection instance with the pymysql parent __init__ patched out."""
23
+ with patch.object(pymysql._Connection, "__init__", return_value=None):
24
+ conn = pymysql.Connection.__new__(pymysql.Connection)
25
+ yield conn
26
+
27
+
28
+ class TestSelect:
29
+ def test_execute_returns_cursor(self, mock_conn):
30
+ conn, cursor = mock_conn
31
+ result = Select(conn, "id", "name").From("users").execute()
32
+ assert result is cursor
33
+
34
+ def test_execute_calls_cursor_execute_with_sql_and_params(self, mock_conn):
35
+ conn, cursor = mock_conn
36
+ q = Select(conn, "name").From("users").Where("id = ?", 1)
37
+ q.execute()
38
+ cursor.execute.assert_called_once_with(q.sql, q.params)
39
+
40
+ def test_execute_does_not_use_context_manager(self, mock_conn):
41
+ conn, cursor = mock_conn
42
+ Select(conn, "id").From("users").execute()
43
+ cursor.__enter__.assert_not_called()
44
+
45
+ def test_fetchone_returns_result(self, mock_conn):
46
+ conn, cursor = mock_conn
47
+ cursor.fetchone.return_value = {"id": 1, "name": "Alice"}
48
+ result = Select(conn, "id", "name").From("users").Where("id = ?", 1).fetchone()
49
+ assert result == {"id": 1, "name": "Alice"}
50
+
51
+ def test_fetchone_returns_none_when_no_row(self, mock_conn):
52
+ conn, cursor = mock_conn
53
+ cursor.fetchone.return_value = None
54
+ result = Select(conn, "id").From("users").Where("id = ?", 999).fetchone()
55
+ assert result is None
56
+
57
+ def test_fetchone_calls_cursor_execute(self, mock_conn):
58
+ conn, cursor = mock_conn
59
+ q = Select(conn, "name").From("users").Where("id = ?", 1)
60
+ q.fetchone()
61
+ cursor.execute.assert_called_once_with(q.sql, q.params)
62
+
63
+ def test_fetchone_uses_context_manager(self, mock_conn):
64
+ conn, cursor = mock_conn
65
+ Select(conn, "name").From("users").fetchone()
66
+ cursor.__enter__.assert_called_once()
67
+
68
+ def test_fetchall_returns_result(self, mock_conn):
69
+ conn, cursor = mock_conn
70
+ cursor.fetchall.return_value = [{"id": 1}, {"id": 2}]
71
+ result = Select(conn, "id").From("users").fetchall()
72
+ assert result == [{"id": 1}, {"id": 2}]
73
+
74
+ def test_fetchall_returns_empty_list(self, mock_conn):
75
+ conn, cursor = mock_conn
76
+ cursor.fetchall.return_value = []
77
+ result = Select(conn, "id").From("users").fetchall()
78
+ assert result == []
79
+
80
+ def test_fetchall_calls_cursor_execute(self, mock_conn):
81
+ conn, cursor = mock_conn
82
+ q = Select(conn, "id").From("users")
83
+ q.fetchall()
84
+ cursor.execute.assert_called_once_with(q.sql, q.params)
85
+
86
+ def test_fetchall_uses_context_manager(self, mock_conn):
87
+ conn, cursor = mock_conn
88
+ Select(conn, "id").From("users").fetchall()
89
+ cursor.__enter__.assert_called_once()
90
+
91
+ def test_multiple_where_params_passed_correctly(self, mock_conn):
92
+ conn, cursor = mock_conn
93
+ q = Select(conn, "name").From("users").Where("age > ?", 18).Where("active = ?", True)
94
+ q.fetchall()
95
+ cursor.execute.assert_called_once_with(q.sql, [18, True])
96
+
97
+ def test_chaining_with_order_and_limit(self, mock_conn):
98
+ conn, cursor = mock_conn
99
+ cursor.fetchall.return_value = [{"name": "Alice"}]
100
+ result = Select(conn, "name").From("users").Where("age >= ?", 18).OrderBy("name").Limit(1).fetchall()
101
+ assert result == [{"name": "Alice"}]
102
+
103
+
104
+ class TestInsert:
105
+ def test_execute_returns_rowcount(self, mock_conn):
106
+ conn, cursor = mock_conn
107
+ cursor.rowcount = 1
108
+ count = Insert(conn).Into("users").Values(id=1, name="Alice").execute()
109
+ assert count == 1
110
+
111
+ def test_execute_calls_cursor_execute(self, mock_conn):
112
+ conn, cursor = mock_conn
113
+ q = Insert(conn).Into("users").Values(id=1, name="Alice")
114
+ q.execute()
115
+ cursor.execute.assert_called_once_with(q.sql, q.params)
116
+
117
+ def test_execute_uses_context_manager(self, mock_conn):
118
+ conn, cursor = mock_conn
119
+ Insert(conn).Into("users").Values(id=1, name="Alice").execute()
120
+ cursor.__enter__.assert_called_once()
121
+
122
+ def test_execute_multiple_rows_rowcount(self, mock_conn):
123
+ conn, cursor = mock_conn
124
+ cursor.rowcount = 2
125
+ count = Insert(conn).Into("users").Values(id=1, name="Alice").Values(id=2, name="Bob").execute()
126
+ assert count == 2
127
+
128
+
129
+ class TestUpdate:
130
+ def test_execute_returns_rowcount(self, mock_conn):
131
+ conn, cursor = mock_conn
132
+ cursor.rowcount = 1
133
+ count = Update(conn, "users").Set(name="Alice").Where("id = ?", 1).execute()
134
+ assert count == 1
135
+
136
+ def test_execute_calls_cursor_execute(self, mock_conn):
137
+ conn, cursor = mock_conn
138
+ q = Update(conn, "users").Set(name="Alice").Where("id = ?", 1)
139
+ q.execute()
140
+ cursor.execute.assert_called_once_with(q.sql, q.params)
141
+
142
+ def test_execute_uses_context_manager(self, mock_conn):
143
+ conn, cursor = mock_conn
144
+ Update(conn, "users").Set(name="Alice").execute()
145
+ cursor.__enter__.assert_called_once()
146
+
147
+ def test_execute_no_where_returns_affected_rowcount(self, mock_conn):
148
+ conn, cursor = mock_conn
149
+ cursor.rowcount = 5
150
+ count = Update(conn, "users").Set(active=False).execute()
151
+ assert count == 5
152
+
153
+ def test_execute_multiple_set_columns(self, mock_conn):
154
+ conn, cursor = mock_conn
155
+ cursor.rowcount = 1
156
+ q = Update(conn, "users").Set(name="Bob", age=20).Where("id = ?", 2)
157
+ count = q.execute()
158
+ cursor.execute.assert_called_once_with(q.sql, q.params)
159
+ assert count == 1
160
+
161
+
162
+ class TestDelete:
163
+ def test_execute_returns_rowcount(self, mock_conn):
164
+ conn, cursor = mock_conn
165
+ cursor.rowcount = 1
166
+ count = Delete(conn).From("users").Where("id = ?", 1).execute()
167
+ assert count == 1
168
+
169
+ def test_execute_calls_cursor_execute(self, mock_conn):
170
+ conn, cursor = mock_conn
171
+ q = Delete(conn).From("users").Where("id = ?", 1)
172
+ q.execute()
173
+ cursor.execute.assert_called_once_with(q.sql, q.params)
174
+
175
+ def test_execute_uses_context_manager(self, mock_conn):
176
+ conn, cursor = mock_conn
177
+ Delete(conn).From("users").Where("id = ?", 1).execute()
178
+ cursor.__enter__.assert_called_once()
179
+
180
+ def test_execute_no_where_returns_all_rowcount(self, mock_conn):
181
+ conn, cursor = mock_conn
182
+ cursor.rowcount = 3
183
+ count = Delete(conn).From("users").execute()
184
+ assert count == 3
185
+
186
+ def test_execute_with_limit(self, mock_conn):
187
+ conn, cursor = mock_conn
188
+ cursor.rowcount = 2
189
+ q = Delete(conn).From("users").Limit(2)
190
+ count = q.execute()
191
+ cursor.execute.assert_called_once_with(q.sql, q.params)
192
+ assert count == 2
193
+
194
+
195
+ class TestConnection:
196
+ def test_default_cursorclass_is_dictcursor(self):
197
+ with patch.object(pymysql._Connection, "__init__", return_value=None) as mock_init:
198
+ Connection(host="localhost", user="root", password="secret", db="mydb")
199
+ kwargs = mock_init.call_args[1]
200
+ assert kwargs["cursorclass"] is DictCursor
201
+
202
+ def test_custom_cursorclass_is_not_overridden(self):
203
+ custom_cursor = MagicMock()
204
+ with patch.object(pymysql._Connection, "__init__", return_value=None) as mock_init:
205
+ Connection(host="localhost", cursorclass=custom_cursor)
206
+ kwargs = mock_init.call_args[1]
207
+ assert kwargs["cursorclass"] is custom_cursor
208
+
209
+ def test_select_returns_select_instance(self, connection):
210
+ result = connection.Select("id", "name")
211
+ assert isinstance(result, Select)
212
+
213
+ def test_insert_returns_insert_instance(self, connection):
214
+ result = connection.Insert()
215
+ assert isinstance(result, Insert)
216
+
217
+ def test_delete_returns_delete_instance(self, connection):
218
+ result = connection.Delete()
219
+ assert isinstance(result, Delete)
220
+
221
+ def test_update_returns_update_instance(self, connection):
222
+ result = connection.Update("users")
223
+ assert isinstance(result, Update)
@@ -0,0 +1,226 @@
1
+ import pytest
2
+ from sqlalchemy import text
3
+ from sqlalchemy.engine import Connection as _SAConnection, Engine as _SAEngine
4
+ from sqlalchemy.pool import StaticPool
5
+
6
+ from sqlqb.sqlalchemy import Connection, Engine, create_engine
7
+
8
+
9
+ @pytest.fixture(scope="module")
10
+ def engine():
11
+ e = create_engine(
12
+ "sqlite:///:memory:",
13
+ connect_args={"check_same_thread": False},
14
+ poolclass=StaticPool,
15
+ )
16
+ with e.connect() as conn:
17
+ conn.execute(text("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"))
18
+ conn.Insert().Into("users").Values(id=1, name="Alice", age=30).execute()
19
+ conn.Insert().Into("users").Values(id=2, name="Bob", age=17).execute()
20
+ conn.Insert().Into("users").Values(id=3, name="Carol", age=25).execute()
21
+ conn.commit()
22
+ return e
23
+
24
+
25
+ @pytest.fixture
26
+ def conn(engine):
27
+ with engine.connect() as c:
28
+ yield c
29
+ c.rollback()
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Engine
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ class TestEngine:
38
+ def test_create_engine_returns_engine_subclass(self):
39
+ e = create_engine("sqlite:///:memory:")
40
+ assert isinstance(e, Engine)
41
+ assert isinstance(e, _SAEngine)
42
+
43
+ def test_connect_returns_connection_subclass(self, engine):
44
+ with engine.connect() as conn:
45
+ assert isinstance(conn, Connection)
46
+ assert isinstance(conn, _SAConnection)
47
+
48
+ def test_connect_as_context_manager(self, engine):
49
+ with engine.connect() as conn:
50
+ assert conn is not None
51
+
52
+ def test_connect_without_context_manager(self, engine):
53
+ conn = engine.connect()
54
+ assert isinstance(conn, Connection)
55
+ conn.close()
56
+
57
+ def test_engine_dialect_accessible(self):
58
+ e = create_engine("sqlite:///:memory:")
59
+ assert e.dialect.name == "sqlite"
60
+
61
+ def test_engine_dispose(self):
62
+ e = create_engine("sqlite:///:memory:")
63
+ e.dispose()
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Connection
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ class TestConnection:
72
+ def test_sa_methods_are_accessible(self, conn):
73
+ assert callable(conn.commit)
74
+ assert callable(conn.rollback)
75
+ assert callable(conn.close)
76
+ assert callable(conn.execute)
77
+
78
+ def test_select_returns_select_instance(self, conn):
79
+ from sqlqb.sqlalchemy import Select
80
+
81
+ assert isinstance(conn.Select("id"), Select)
82
+
83
+ def test_insert_returns_insert_instance(self, conn):
84
+ from sqlqb.sqlalchemy import Insert
85
+
86
+ assert isinstance(conn.Insert(), Insert)
87
+
88
+ def test_update_returns_update_instance(self, conn):
89
+ from sqlqb.sqlalchemy import Update
90
+
91
+ assert isinstance(conn.Update("users"), Update)
92
+
93
+ def test_delete_returns_delete_instance(self, conn):
94
+ from sqlqb.sqlalchemy import Delete
95
+
96
+ assert isinstance(conn.Delete(), Delete)
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Select
101
+ # ---------------------------------------------------------------------------
102
+
103
+
104
+ class TestSelect:
105
+ def test_fetchall(self, conn):
106
+ rows = conn.Select("id", "name").From("users").fetchall()
107
+ assert rows == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Carol"}]
108
+
109
+ def test_fetchone(self, conn):
110
+ row = conn.Select("name").From("users").Where("id = ?", 1).fetchone()
111
+ assert row == {"name": "Alice"}
112
+
113
+ def test_fetchone_returns_none_when_no_row(self, conn):
114
+ row = conn.Select("name").From("users").Where("id = ?", 999).fetchone()
115
+ assert row is None
116
+
117
+ def test_fetchall_returns_empty_list(self, conn):
118
+ rows = conn.Select("name").From("users").Where("id = ?", 999).fetchall()
119
+ assert rows == []
120
+
121
+ def test_where_single_param(self, conn):
122
+ rows = conn.Select("name").From("users").Where("age >= ?", 18).fetchall()
123
+ assert rows == [{"name": "Alice"}, {"name": "Carol"}]
124
+
125
+ def test_where_multiple_params(self, conn):
126
+ rows = conn.Select("name").From("users").Where("age >= ?", 18).Where("age <= ?", 29).fetchall()
127
+ assert rows == [{"name": "Carol"}]
128
+
129
+ def test_order_by_asc(self, conn):
130
+ rows = conn.Select("name").From("users").OrderBy("name").fetchall()
131
+ assert rows == [{"name": "Alice"}, {"name": "Bob"}, {"name": "Carol"}]
132
+
133
+ def test_order_by_desc(self, conn):
134
+ rows = conn.Select("name").From("users").OrderBy("age", "DESC").fetchall()
135
+ assert rows == [{"name": "Alice"}, {"name": "Carol"}, {"name": "Bob"}]
136
+
137
+ def test_limit(self, conn):
138
+ rows = conn.Select("id").From("users").Limit(2).fetchall()
139
+ assert rows == [{"id": 1}, {"id": 2}]
140
+
141
+ def test_execute_returns_sa_result(self, conn):
142
+ result = conn.Select("id", "name").From("users").execute()
143
+ rows = [dict(r) for r in result.mappings()]
144
+ assert rows == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Carol"}]
145
+
146
+ def test_fetchall_returns_plain_dicts(self, conn):
147
+ rows = conn.Select("id").From("users").Limit(1).fetchall()
148
+ assert type(rows[0]) is dict
149
+
150
+ def test_fetchone_returns_plain_dict(self, conn):
151
+ row = conn.Select("id").From("users").Where("id = ?", 1).fetchone()
152
+ assert type(row) is dict
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Insert
157
+ # ---------------------------------------------------------------------------
158
+
159
+
160
+ class TestInsert:
161
+ def test_execute_returns_rowcount(self, conn):
162
+ count = conn.Insert().Into("users").Values(id=4, name="Dave", age=40).execute()
163
+ assert count == 1
164
+
165
+ def test_inserted_row_is_queryable(self, conn):
166
+ conn.Insert().Into("users").Values(id=4, name="Dave", age=40).execute()
167
+ row = conn.Select("name").From("users").Where("id = ?", 4).fetchone()
168
+ assert row == {"name": "Dave"}
169
+
170
+ def test_insert_multiple_rows(self, conn):
171
+ count = conn.Insert().Into("users").Values(id=4, name="Dave", age=40).Values(id=5, name="Eve", age=22).execute()
172
+ assert count == 2
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Update
177
+ # ---------------------------------------------------------------------------
178
+
179
+
180
+ class TestUpdate:
181
+ def test_execute_returns_rowcount(self, conn):
182
+ count = conn.Update("users").Set(name="Alicia").Where("id = ?", 1).execute()
183
+ assert count == 1
184
+
185
+ def test_update_is_reflected(self, conn):
186
+ conn.Update("users").Set(name="Alicia").Where("id = ?", 1).execute()
187
+ row = conn.Select("name").From("users").Where("id = ?", 1).fetchone()
188
+ assert row == {"name": "Alicia"}
189
+
190
+ def test_update_multiple_columns(self, conn):
191
+ conn.Update("users").Set(name="Bobby", age=18).Where("id = ?", 2).execute()
192
+ row = conn.Select("name", "age").From("users").Where("id = ?", 2).fetchone()
193
+ assert row == {"name": "Bobby", "age": 18}
194
+
195
+ def test_update_no_where_affects_all(self, conn):
196
+ count = conn.Update("users").Set(age=0).execute()
197
+ assert count == 3
198
+
199
+ def test_update_affects_correct_rows_only(self, conn):
200
+ conn.Update("users").Set(age=99).Where("id = ?", 1).execute()
201
+ rows = conn.Select("id", "age").From("users").fetchall()
202
+ assert rows == [{"id": 1, "age": 99}, {"id": 2, "age": 17}, {"id": 3, "age": 25}]
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Delete
207
+ # ---------------------------------------------------------------------------
208
+
209
+
210
+ class TestDelete:
211
+ def test_execute_returns_rowcount(self, conn):
212
+ conn.Insert().Into("users").Values(id=4, name="Temp", age=1).execute()
213
+ count = conn.Delete().From("users").Where("id = ?", 4).execute()
214
+ assert count == 1
215
+
216
+ def test_deleted_row_is_gone(self, conn):
217
+ conn.Insert().Into("users").Values(id=4, name="Temp", age=1).execute()
218
+ conn.Delete().From("users").Where("id = ?", 4).execute()
219
+ row = conn.Select("*").From("users").Where("id = ?", 4).fetchone()
220
+ assert row is None
221
+
222
+ def test_delete_no_where_clears_table(self, conn):
223
+ count = conn.Delete().From("users").execute()
224
+ assert count == 3
225
+ rows = conn.Select("*").From("users").fetchall()
226
+ assert rows == []
@@ -30,13 +30,7 @@ class TestSelect:
30
30
  assert cursor.fetchone() == {"id": 1, "name": "Alice", "age": 30}
31
31
 
32
32
  def test_chaining(self, conn):
33
- rows = (
34
- conn.Select("name")
35
- .From("users")
36
- .Where("age >= ?", 18)
37
- .OrderBy("name")
38
- .fetchall()
39
- )
33
+ rows = conn.Select("name").From("users").Where("age >= ?", 18).OrderBy("name").fetchall()
40
34
  assert rows == [{"name": "Alice"}, {"name": "Carol"}]
41
35
 
42
36
  def test_limit(self, conn):
@@ -108,5 +102,6 @@ class TestDelete:
108
102
  def test_delete_with_limit_not_supported(self, conn):
109
103
  # NOTE: sqlite library of python does not support LIMIT in delete queries
110
104
  import sqlite3
105
+
111
106
  with pytest.raises(sqlite3.OperationalError):
112
107
  conn.Delete().From("users").Limit(2).execute()
@@ -95,6 +95,61 @@ wheels = [
95
95
  { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
96
96
  ]
97
97
 
98
+ [[package]]
99
+ name = "greenlet"
100
+ version = "3.5.2"
101
+ source = { registry = "https://pypi.org/simple" }
102
+ sdist = { url = "https://files.pythonhosted.org/packages/dd/8b/befc3cb36965f397d87e86fb3b00e3ec0dc67c1ecb0986d7f54ee528f018/greenlet-3.5.2.tar.gz", hash = "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", size = 199243, upload-time = "2026-06-17T20:19:01.317Z" }
103
+ wheels = [
104
+ { url = "https://files.pythonhosted.org/packages/3f/7a/6bc2a7835731387ed303b9390ce68a116ab053df05450a59181239200454/greenlet-3.5.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f", size = 288351, upload-time = "2026-06-17T17:36:17.019Z" },
105
+ { url = "https://files.pythonhosted.org/packages/57/1b/bd98062fcef6d0e9d0873ab6f2d029772e6ea342972ae43275bd6177900f/greenlet-3.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742", size = 604273, upload-time = "2026-06-17T18:07:20.296Z" },
106
+ { url = "https://files.pythonhosted.org/packages/25/e6/fe392c522bf45d976abe7db2793f6ef4e87b053ebb869deeaae46aeb54da/greenlet-3.5.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea", size = 616536, upload-time = "2026-06-17T18:29:43.205Z" },
107
+ { url = "https://files.pythonhosted.org/packages/68/4a/399ff81fa93a19d6a9df394cef0355f082dbc19ad41aba9593cd0ad444e2/greenlet-3.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3", size = 613749, upload-time = "2026-06-17T17:39:28.148Z" },
108
+ { url = "https://files.pythonhosted.org/packages/a5/75/f519593f12ad43d08e28c03a95cfe2eeae011707dbc9dab0c4a263ce90f9/greenlet-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094", size = 1573725, upload-time = "2026-06-17T18:22:12.023Z" },
109
+ { url = "https://files.pythonhosted.org/packages/f1/bc/bc1ea4b0754c6c51bbf9d94677b0b1f7fbda8cbb404e44a896854fc0a940/greenlet-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad", size = 1638132, upload-time = "2026-06-17T17:40:06.971Z" },
110
+ { url = "https://files.pythonhosted.org/packages/36/c0/f0f5a34247df60de285f75f22e57f14027f4b3c43820981854b5b643ca6d/greenlet-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146", size = 239393, upload-time = "2026-06-17T17:33:47.09Z" },
111
+ { url = "https://files.pythonhosted.org/packages/09/17/a8544e165445f30aea67a8d9cf2786d2bb0eb1b0e0d224b4d9bd80e2d587/greenlet-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9", size = 237723, upload-time = "2026-06-17T17:36:47.776Z" },
112
+ { url = "https://files.pythonhosted.org/packages/d0/3c/bb37b9d40d65b0741a8b040ca5c307034d0a9822994dff5f825c88dd7a6b/greenlet-3.5.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", size = 287178, upload-time = "2026-06-17T17:35:25.132Z" },
113
+ { url = "https://files.pythonhosted.org/packages/f0/a6/0c5902393f492f8ceb19d0b5cf139284e3a11b333a049739643b1036b6f8/greenlet-3.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", size = 606900, upload-time = "2026-06-17T18:07:21.692Z" },
114
+ { url = "https://files.pythonhosted.org/packages/d8/7c/42899c31d4b87148ae4e3f87f63e13398824be6241f4dde42ded95768a34/greenlet-3.5.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", size = 619265, upload-time = "2026-06-17T18:29:44.837Z" },
115
+ { url = "https://files.pythonhosted.org/packages/d3/52/4ff8c98d3cfe62b4515f8584ae14510a58f35c549cc5292b78d9b7a40b70/greenlet-3.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", size = 616187, upload-time = "2026-06-17T17:39:29.473Z" },
116
+ { url = "https://files.pythonhosted.org/packages/c9/a6/269c8bf9aefc13361ce1088f0e392b154cb21005de7862e42b5d782b81fd/greenlet-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", size = 1573778, upload-time = "2026-06-17T18:22:13.497Z" },
117
+ { url = "https://files.pythonhosted.org/packages/1f/9b/391d015cbc6323e81b14c02cf825fdca7e0049c9bb489bf4ac72883118ba/greenlet-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", size = 1638092, upload-time = "2026-06-17T17:40:08.163Z" },
118
+ { url = "https://files.pythonhosted.org/packages/49/53/5b4df711f4356c62e85d9f819d87966d526d1cfb32bae49a8f7d6fc36ea4/greenlet-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", size = 239352, upload-time = "2026-06-17T17:38:51.593Z" },
119
+ { url = "https://files.pythonhosted.org/packages/bb/b6/18efc3a329ec035c3f344b8f2b60356451950ddf9b7b64ff00023778a1dd/greenlet-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", size = 237635, upload-time = "2026-06-17T17:35:36.632Z" },
120
+ { url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" },
121
+ { url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" },
122
+ { url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" },
123
+ { url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" },
124
+ { url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" },
125
+ { url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" },
126
+ { url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" },
127
+ { url = "https://files.pythonhosted.org/packages/de/e3/3b62145fe917311732041a258adb218248add00542e3131c48bd047fbed5/greenlet-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", size = 239038, upload-time = "2026-06-17T17:37:56.792Z" },
128
+ { url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" },
129
+ { url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" },
130
+ { url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" },
131
+ { url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" },
132
+ { url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" },
133
+ { url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" },
134
+ { url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" },
135
+ { url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" },
136
+ { url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" },
137
+ { url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" },
138
+ { url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" },
139
+ { url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" },
140
+ { url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" },
141
+ { url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" },
142
+ { url = "https://files.pythonhosted.org/packages/7f/4d/86d7768bd53e9907de0333df215c2018cd01a593b3715cbd79aa82dd94b7/greenlet-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", size = 239579, upload-time = "2026-06-17T17:39:39.954Z" },
143
+ { url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" },
144
+ { url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" },
145
+ { url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" },
146
+ { url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" },
147
+ { url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" },
148
+ { url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" },
149
+ { url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" },
150
+ { url = "https://files.pythonhosted.org/packages/37/45/f794a81c91e9942c61f9110bd1f9a38a0ea565eab57f8b08cd53d3131e48/greenlet-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", size = 242062, upload-time = "2026-06-17T17:35:39.814Z" },
151
+ ]
152
+
98
153
  [[package]]
99
154
  name = "iniconfig"
100
155
  version = "2.3.0"
@@ -131,6 +186,15 @@ wheels = [
131
186
  { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
132
187
  ]
133
188
 
189
+ [[package]]
190
+ name = "pymysql"
191
+ version = "1.2.0"
192
+ source = { registry = "https://pypi.org/simple" }
193
+ sdist = { url = "https://files.pythonhosted.org/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" }
194
+ wheels = [
195
+ { url = "https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" },
196
+ ]
197
+
134
198
  [[package]]
135
199
  name = "pytest"
136
200
  version = "9.0.3"
@@ -186,9 +250,50 @@ wheels = [
186
250
  { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
187
251
  ]
188
252
 
253
+ [[package]]
254
+ name = "sqlalchemy"
255
+ version = "2.0.51"
256
+ source = { registry = "https://pypi.org/simple" }
257
+ dependencies = [
258
+ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
259
+ { name = "typing-extensions" },
260
+ ]
261
+ sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" }
262
+ wheels = [
263
+ { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" },
264
+ { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" },
265
+ { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" },
266
+ { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" },
267
+ { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" },
268
+ { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" },
269
+ { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" },
270
+ { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" },
271
+ { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" },
272
+ { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" },
273
+ { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" },
274
+ { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" },
275
+ { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" },
276
+ { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" },
277
+ { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" },
278
+ { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" },
279
+ { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" },
280
+ { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" },
281
+ { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" },
282
+ { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" },
283
+ { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" },
284
+ { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" },
285
+ { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" },
286
+ { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" },
287
+ { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" },
288
+ { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" },
289
+ { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" },
290
+ { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" },
291
+ { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
292
+ ]
293
+
189
294
  [[package]]
190
295
  name = "sqlqb"
191
- version = "0.1.2"
296
+ version = "0.1.3"
192
297
  source = { editable = "." }
193
298
 
194
299
  [package.dev-dependencies]
@@ -197,6 +302,12 @@ dev = [
197
302
  { name = "pytest-cov" },
198
303
  { name = "ruff" },
199
304
  ]
305
+ pymysql = [
306
+ { name = "pymysql" },
307
+ ]
308
+ sqlalchemy = [
309
+ { name = "sqlalchemy" },
310
+ ]
200
311
 
201
312
  [package.metadata]
202
313
 
@@ -206,3 +317,14 @@ dev = [
206
317
  { name = "pytest-cov", specifier = ">=7.1.0" },
207
318
  { name = "ruff", specifier = ">=0.15.17" },
208
319
  ]
320
+ pymysql = [{ name = "pymysql", specifier = ">=1.2.0" }]
321
+ sqlalchemy = [{ name = "sqlalchemy", specifier = ">=2.0.51" }]
322
+
323
+ [[package]]
324
+ name = "typing-extensions"
325
+ version = "4.15.0"
326
+ source = { registry = "https://pypi.org/simple" }
327
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
328
+ wheels = [
329
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
330
+ ]
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