sqlqb 0.1.4__tar.gz → 0.2.1__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.
- {sqlqb-0.1.4 → sqlqb-0.2.1}/Makefile +2 -1
- sqlqb-0.2.1/PKG-INFO +5 -0
- {sqlqb-0.1.4 → sqlqb-0.2.1}/pyproject.toml +8 -6
- {sqlqb-0.1.4 → sqlqb-0.2.1}/src/sqlqb/__init__.py +3 -3
- sqlqb-0.2.1/src/sqlqb/delete.py +36 -0
- {sqlqb-0.1.4 → sqlqb-0.2.1}/src/sqlqb/insert.py +8 -10
- {sqlqb-0.1.4 → sqlqb-0.2.1}/src/sqlqb/select.py +13 -33
- {sqlqb-0.1.4 → sqlqb-0.2.1}/src/sqlqb/sqlalchemy/__init__.py +6 -24
- {sqlqb-0.1.4 → sqlqb-0.2.1}/src/sqlqb/sqlite3/__init__.py +8 -3
- sqlqb-0.2.1/src/sqlqb/update.py +43 -0
- sqlqb-0.2.1/tests/test_delete.py +62 -0
- {sqlqb-0.1.4 → sqlqb-0.2.1}/tests/test_insert.py +12 -12
- {sqlqb-0.1.4 → sqlqb-0.2.1}/tests/test_pymysql.py +42 -28
- {sqlqb-0.1.4 → sqlqb-0.2.1}/tests/test_select.py +24 -24
- {sqlqb-0.1.4 → sqlqb-0.2.1}/tests/test_sqlalchemy.py +18 -17
- {sqlqb-0.1.4 → sqlqb-0.2.1}/tests/test_sqlite3.py +26 -23
- {sqlqb-0.1.4 → sqlqb-0.2.1}/tests/test_update.py +25 -36
- sqlqb-0.2.1/uv.lock +332 -0
- sqlqb-0.1.4/PKG-INFO +0 -5
- sqlqb-0.1.4/data.db +0 -0
- sqlqb-0.1.4/src/sqlqb/delete.py +0 -57
- sqlqb-0.1.4/src/sqlqb/update.py +0 -69
- sqlqb-0.1.4/tests/test_delete.py +0 -81
- sqlqb-0.1.4/uv.lock +0 -330
- {sqlqb-0.1.4 → sqlqb-0.2.1}/.envrc +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.1}/.gitignore +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.1}/.pythonrc.py +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.1}/README.md +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.1}/src/sqlqb/pymysql/__init__.py +2 -2
- {sqlqb-0.1.4 → sqlqb-0.2.1}/tests/__init__.py +0 -0
sqlqb-0.2.1/PKG-INFO
ADDED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlqb"
|
|
3
|
-
|
|
4
|
-
description = "
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "a simple sql query builder"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = []
|
|
7
|
-
requires-python = ">=3.12"
|
|
7
|
+
requires-python = ">= 3.12"
|
|
8
8
|
dependencies = []
|
|
9
9
|
|
|
10
10
|
[dependency-groups]
|
|
11
11
|
dev = [
|
|
12
|
-
"pytest>=9.0.3",
|
|
13
12
|
"pytest-cov>=7.1.0",
|
|
13
|
+
"pytest>=9.0.3",
|
|
14
14
|
"ruff>=0.15.17",
|
|
15
15
|
]
|
|
16
16
|
pymysql = [
|
|
@@ -31,7 +31,9 @@ source = "vcs"
|
|
|
31
31
|
testpaths = ["tests"]
|
|
32
32
|
|
|
33
33
|
[tool.ruff]
|
|
34
|
-
line-length =
|
|
34
|
+
line-length = 320
|
|
35
35
|
|
|
36
36
|
[tool.ruff.lint]
|
|
37
|
-
ignore = [
|
|
37
|
+
ignore = [
|
|
38
|
+
"E501", # E501 is the line-too-long rule
|
|
39
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from .select import Select
|
|
2
|
-
from .insert import Insert
|
|
3
1
|
from .delete import Delete
|
|
2
|
+
from .insert import Insert
|
|
3
|
+
from .select import Select
|
|
4
4
|
from .update import Update
|
|
5
5
|
|
|
6
|
-
__all__ = ["
|
|
6
|
+
__all__ = ["Delete", "Insert", "Select", "Update"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Delete:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.__table: str | None = None
|
|
7
|
+
self.__wheres: list[str] = []
|
|
8
|
+
self.__params: dict = {}
|
|
9
|
+
|
|
10
|
+
def From(self, table: str) -> "Delete":
|
|
11
|
+
self.__table = table
|
|
12
|
+
return self
|
|
13
|
+
|
|
14
|
+
def Where(self, condition: str, **kwargs) -> "Delete":
|
|
15
|
+
self.__wheres.append(condition)
|
|
16
|
+
self.__params.update(kwargs)
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def params(self) -> dict:
|
|
21
|
+
return self.__params
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def sql(self) -> str:
|
|
25
|
+
if not self.__table or not str(self.__table).strip():
|
|
26
|
+
raise ValueError("error: table name must not be empty")
|
|
27
|
+
sql = io.StringIO()
|
|
28
|
+
sql.write("DELETE FROM ")
|
|
29
|
+
sql.write(self.__table)
|
|
30
|
+
if self.__wheres:
|
|
31
|
+
sql.write(" WHERE ")
|
|
32
|
+
sql.write(" AND ".join(self.__wheres))
|
|
33
|
+
return sql.getvalue()
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
return self.sql
|
|
@@ -4,7 +4,7 @@ import io
|
|
|
4
4
|
class Insert:
|
|
5
5
|
def __init__(self):
|
|
6
6
|
self.__table: str | None = None
|
|
7
|
-
self.
|
|
7
|
+
self.__values: list[dict] = []
|
|
8
8
|
|
|
9
9
|
def Into(self, table: str) -> "Insert":
|
|
10
10
|
if not table or not table.strip():
|
|
@@ -15,30 +15,28 @@ class Insert:
|
|
|
15
15
|
def Values(self, **kwargs) -> "Insert":
|
|
16
16
|
if not kwargs:
|
|
17
17
|
raise ValueError("error: specify at least one column value")
|
|
18
|
-
self.
|
|
18
|
+
self.__values.append(kwargs)
|
|
19
19
|
return self
|
|
20
20
|
|
|
21
21
|
@property
|
|
22
22
|
def params(self) -> list:
|
|
23
|
-
|
|
24
|
-
return []
|
|
25
|
-
return [v for row in self.__rows for v in row.values()]
|
|
23
|
+
return self.__values
|
|
26
24
|
|
|
27
25
|
@property
|
|
28
26
|
def sql(self) -> str:
|
|
29
27
|
if not self.__table:
|
|
30
28
|
raise ValueError("error: no table specified")
|
|
31
|
-
if not self.
|
|
29
|
+
if not self.__values:
|
|
32
30
|
raise ValueError("error: no values specified")
|
|
33
|
-
columns =
|
|
31
|
+
columns = self.__values[0].keys()
|
|
34
32
|
sql = io.StringIO()
|
|
35
33
|
sql.write("INSERT INTO ")
|
|
36
34
|
sql.write(self.__table)
|
|
37
35
|
sql.write(" (")
|
|
38
36
|
sql.write(", ".join(columns))
|
|
39
|
-
sql.write(") VALUES ")
|
|
40
|
-
|
|
41
|
-
sql.write("
|
|
37
|
+
sql.write(") VALUES (")
|
|
38
|
+
sql.write(", ".join([f":{column}" for column in columns])) # TODO this assumes :style parameters
|
|
39
|
+
sql.write(")")
|
|
42
40
|
return sql.getvalue()
|
|
43
41
|
|
|
44
42
|
def __str__(self) -> str:
|
|
@@ -3,21 +3,20 @@ import io
|
|
|
3
3
|
|
|
4
4
|
class Select:
|
|
5
5
|
def __init__(self, *args: str):
|
|
6
|
-
self.__columns:
|
|
6
|
+
self.__columns: tuple[str] = args
|
|
7
7
|
self.__table: str | None = None
|
|
8
|
-
self.__wheres: list[str] = []
|
|
9
|
-
self.__params: list = []
|
|
10
|
-
self.__named_params: dict = {}
|
|
11
8
|
self.__joins: list[str] = []
|
|
9
|
+
self.__wheres: list[str] = []
|
|
12
10
|
self.__orderby: list[tuple[str, str]] = []
|
|
13
11
|
self.__groupby: list[str] = []
|
|
14
12
|
self.__limit: int | None = None
|
|
15
13
|
self.__offset: int | None = None
|
|
14
|
+
self.__params: dict = {}
|
|
16
15
|
|
|
17
16
|
def Columns(self, *columns: str) -> "Select":
|
|
18
17
|
if not columns:
|
|
19
18
|
raise ValueError("error: specify at least one column")
|
|
20
|
-
self.__columns =
|
|
19
|
+
self.__columns = columns
|
|
21
20
|
return self
|
|
22
21
|
|
|
23
22
|
def From(self, table: str) -> "Select":
|
|
@@ -26,32 +25,14 @@ class Select:
|
|
|
26
25
|
self.__table = table
|
|
27
26
|
return self
|
|
28
27
|
|
|
29
|
-
def Join(self, join: str,
|
|
30
|
-
if args and kwargs:
|
|
31
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
32
|
-
if args and self.__named_params:
|
|
33
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
34
|
-
if kwargs and self.__params:
|
|
35
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
28
|
+
def Join(self, join: str, **kwargs) -> "Select":
|
|
36
29
|
self.__joins.append(join)
|
|
37
|
-
|
|
38
|
-
self.__named_params.update(kwargs)
|
|
39
|
-
else:
|
|
40
|
-
self.__params += list(args)
|
|
30
|
+
self.__params.update(kwargs)
|
|
41
31
|
return self
|
|
42
32
|
|
|
43
|
-
def Where(self, condition: str,
|
|
44
|
-
if args and kwargs:
|
|
45
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
46
|
-
if args and self.__named_params:
|
|
47
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
48
|
-
if kwargs and self.__params:
|
|
49
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
33
|
+
def Where(self, condition: str, **kwargs) -> "Select":
|
|
50
34
|
self.__wheres.append(condition)
|
|
51
|
-
|
|
52
|
-
self.__named_params.update(kwargs)
|
|
53
|
-
else:
|
|
54
|
-
self.__params += list(args)
|
|
35
|
+
self.__params.update(kwargs)
|
|
55
36
|
return self
|
|
56
37
|
|
|
57
38
|
def OrderBy(self, column: str, direction: str = "ASC") -> "Select":
|
|
@@ -73,19 +54,18 @@ class Select:
|
|
|
73
54
|
return self
|
|
74
55
|
|
|
75
56
|
@property
|
|
76
|
-
def params(self) ->
|
|
77
|
-
if self.__named_params:
|
|
78
|
-
return self.__named_params
|
|
57
|
+
def params(self) -> dict:
|
|
79
58
|
return self.__params
|
|
80
59
|
|
|
81
60
|
@property
|
|
82
61
|
def sql(self) -> str:
|
|
62
|
+
if not self.__table or not str(self.__table).strip():
|
|
63
|
+
raise ValueError("error: table name must not be empty")
|
|
83
64
|
sql = io.StringIO()
|
|
84
65
|
sql.write("SELECT ")
|
|
85
66
|
sql.write(", ".join(self.__columns or "*"))
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
sql.write(self.__table)
|
|
67
|
+
sql.write(" FROM ")
|
|
68
|
+
sql.write(self.__table)
|
|
89
69
|
if self.__joins:
|
|
90
70
|
sql.write(" ")
|
|
91
71
|
sql.write(" ".join(self.__joins))
|
|
@@ -3,22 +3,10 @@ from sqlalchemy import text
|
|
|
3
3
|
from sqlalchemy.engine import Connection as _Connection
|
|
4
4
|
from sqlalchemy.engine import Engine as _Engine
|
|
5
5
|
|
|
6
|
-
from sqlqb import
|
|
6
|
+
from sqlqb import Delete as _Delete
|
|
7
7
|
from sqlqb import Insert as _Insert
|
|
8
|
+
from sqlqb import Select as _Select
|
|
8
9
|
from sqlqb import Update as _Update
|
|
9
|
-
from sqlqb import Delete as _Delete
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _bind(sql: str, params: list | dict) -> tuple[str, dict]:
|
|
13
|
-
"""Convert params for SQLAlchemy text(): pass dicts through, replace ? with :p0/:p1/... for lists."""
|
|
14
|
-
if isinstance(params, dict):
|
|
15
|
-
return sql, params
|
|
16
|
-
named: dict = {}
|
|
17
|
-
for i, val in enumerate(params):
|
|
18
|
-
key = f"p{i}"
|
|
19
|
-
sql = sql.replace("?", f":{key}", 1)
|
|
20
|
-
named[key] = val
|
|
21
|
-
return sql, named
|
|
22
10
|
|
|
23
11
|
|
|
24
12
|
class Select(_Select):
|
|
@@ -27,16 +15,13 @@ class Select(_Select):
|
|
|
27
15
|
self.__connection = connection
|
|
28
16
|
|
|
29
17
|
def execute(self):
|
|
30
|
-
|
|
31
|
-
return self.__connection.execute(text(sql), params)
|
|
18
|
+
return self.__connection.execute(text(self.sql), self.params)
|
|
32
19
|
|
|
33
20
|
def fetchone(self):
|
|
34
|
-
sql, params = _bind(self.sql, self.params)
|
|
35
21
|
row = self.execute().mappings().fetchone()
|
|
36
22
|
return dict(row) if row is not None else None
|
|
37
23
|
|
|
38
24
|
def fetchall(self) -> list:
|
|
39
|
-
sql, params = _bind(self.sql, self.params)
|
|
40
25
|
return [dict(row) for row in self.execute().mappings()]
|
|
41
26
|
|
|
42
27
|
|
|
@@ -46,8 +31,7 @@ class Insert(_Insert):
|
|
|
46
31
|
self.__connection = connection
|
|
47
32
|
|
|
48
33
|
def execute(self) -> int:
|
|
49
|
-
|
|
50
|
-
return self.__connection.execute(text(sql), params).rowcount
|
|
34
|
+
return self.__connection.execute(text(self.sql), self.params).rowcount
|
|
51
35
|
|
|
52
36
|
|
|
53
37
|
class Update(_Update):
|
|
@@ -56,8 +40,7 @@ class Update(_Update):
|
|
|
56
40
|
self.__connection = connection
|
|
57
41
|
|
|
58
42
|
def execute(self) -> int:
|
|
59
|
-
|
|
60
|
-
return self.__connection.execute(text(sql), params).rowcount
|
|
43
|
+
return self.__connection.execute(text(self.sql), self.params).rowcount
|
|
61
44
|
|
|
62
45
|
|
|
63
46
|
class Delete(_Delete):
|
|
@@ -66,8 +49,7 @@ class Delete(_Delete):
|
|
|
66
49
|
self.__connection = connection
|
|
67
50
|
|
|
68
51
|
def execute(self) -> int:
|
|
69
|
-
|
|
70
|
-
return self.__connection.execute(text(sql), params).rowcount
|
|
52
|
+
return self.__connection.execute(text(self.sql), self.params).rowcount
|
|
71
53
|
|
|
72
54
|
|
|
73
55
|
class Connection(_Connection):
|
|
@@ -2,10 +2,10 @@ from sqlite3 import Connection as _Connection
|
|
|
2
2
|
from sqlite3 import Cursor as _Cursor
|
|
3
3
|
from sqlite3 import connect as _connect
|
|
4
4
|
|
|
5
|
-
from sqlqb import
|
|
5
|
+
from sqlqb import Delete as _Delete
|
|
6
6
|
from sqlqb import Insert as _Insert
|
|
7
|
+
from sqlqb import Select as _Select
|
|
7
8
|
from sqlqb import Update as _Update
|
|
8
|
-
from sqlqb import Delete as _Delete
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Select(_Select):
|
|
@@ -29,7 +29,12 @@ class Insert(_Insert):
|
|
|
29
29
|
self.__connection = connection
|
|
30
30
|
|
|
31
31
|
def execute(self) -> int:
|
|
32
|
-
|
|
32
|
+
if len(self.params) == 1:
|
|
33
|
+
return self.__connection.execute(self.sql, self.params[0]).rowcount
|
|
34
|
+
return self.__connection.executemany(self.sql, self.params).rowcount
|
|
35
|
+
|
|
36
|
+
def executemany(self) -> int:
|
|
37
|
+
return self.execute()
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
class Update(_Update):
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Update:
|
|
5
|
+
def __init__(self, table: str):
|
|
6
|
+
if not table or not table.strip():
|
|
7
|
+
raise ValueError("error: table name must not be empty")
|
|
8
|
+
self.__table = table
|
|
9
|
+
self.__sets: dict[str, object] = {}
|
|
10
|
+
self.__wheres: list[str] = []
|
|
11
|
+
self.__params: dict[str, object] = {}
|
|
12
|
+
|
|
13
|
+
def Set(self, **kwargs) -> "Update":
|
|
14
|
+
if not kwargs:
|
|
15
|
+
raise ValueError("error: specify at least one column value")
|
|
16
|
+
self.__sets.update(kwargs)
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def Where(self, condition: str, **kwargs) -> "Update":
|
|
20
|
+
self.__wheres.append(condition)
|
|
21
|
+
self.__params.update(kwargs)
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def params(self) -> dict:
|
|
26
|
+
return self.__sets | self.__params
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def sql(self) -> str:
|
|
30
|
+
if not self.__sets:
|
|
31
|
+
raise ValueError("error: no SET values specified")
|
|
32
|
+
sql = io.StringIO()
|
|
33
|
+
sql.write("UPDATE ")
|
|
34
|
+
sql.write(self.__table)
|
|
35
|
+
sql.write(" SET ")
|
|
36
|
+
sql.write(", ".join([f"{col} = :{col}" for col in self.__sets]))
|
|
37
|
+
if self.__wheres:
|
|
38
|
+
sql.write(" WHERE ")
|
|
39
|
+
sql.write(" AND ".join(self.__wheres))
|
|
40
|
+
return sql.getvalue()
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return self.sql
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from sqlqb import Delete
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestDeleteBasic:
|
|
7
|
+
def test_delete_all(self):
|
|
8
|
+
q = Delete().From("users")
|
|
9
|
+
assert q.sql == "DELETE FROM users"
|
|
10
|
+
|
|
11
|
+
def test_str_equals_sql(self):
|
|
12
|
+
q = Delete().From("users")
|
|
13
|
+
assert str(q) == q.sql
|
|
14
|
+
|
|
15
|
+
def test_no_table_raises(self):
|
|
16
|
+
with pytest.raises(ValueError):
|
|
17
|
+
_ = Delete().sql
|
|
18
|
+
|
|
19
|
+
def test_empty_table_raises(self):
|
|
20
|
+
with pytest.raises(ValueError):
|
|
21
|
+
_ = Delete().From("").sql
|
|
22
|
+
|
|
23
|
+
def test_blank_table_raises(self):
|
|
24
|
+
with pytest.raises(ValueError):
|
|
25
|
+
_ = Delete().From(" ").sql
|
|
26
|
+
|
|
27
|
+
def test_params_empty_by_default(self):
|
|
28
|
+
q = Delete().From("users")
|
|
29
|
+
assert q.params == {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestDeleteWhere:
|
|
33
|
+
def test_single_where(self):
|
|
34
|
+
q = Delete().From("users").Where("id = :id", id=1)
|
|
35
|
+
assert q.sql == "DELETE FROM users WHERE id = :id"
|
|
36
|
+
assert q.params == {"id": 1}
|
|
37
|
+
|
|
38
|
+
def test_multiple_wheres_joined_with_and(self):
|
|
39
|
+
q = Delete().From("users").Where("active = :active", active=False).Where("role = :role", role="guest")
|
|
40
|
+
assert q.sql == "DELETE FROM users WHERE active = :active AND role = :role"
|
|
41
|
+
assert q.params == {"active": False, "role": "guest"}
|
|
42
|
+
|
|
43
|
+
def test_where_no_params(self):
|
|
44
|
+
q = Delete().From("sessions").Where("expired_at IS NOT NULL")
|
|
45
|
+
assert q.sql == "DELETE FROM sessions WHERE expired_at IS NOT NULL"
|
|
46
|
+
assert q.params == {}
|
|
47
|
+
|
|
48
|
+
def test_no_where_absent(self):
|
|
49
|
+
q = Delete().From("users")
|
|
50
|
+
assert "WHERE" not in q.sql
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestDeleteChaining:
|
|
54
|
+
def test_method_chaining_returns_delete(self):
|
|
55
|
+
q = Delete()
|
|
56
|
+
assert q.From("t") is q
|
|
57
|
+
assert q.Where("1=1") is q
|
|
58
|
+
|
|
59
|
+
def test_full_query(self):
|
|
60
|
+
q = Delete().From("logs").Where("level = :level", level="debug")
|
|
61
|
+
assert q.sql == "DELETE FROM logs WHERE level = :level"
|
|
62
|
+
assert q.params == {"level": "debug"}
|
|
@@ -6,7 +6,7 @@ from sqlqb import Insert
|
|
|
6
6
|
class TestInsertBasic:
|
|
7
7
|
def test_single_row(self):
|
|
8
8
|
q = Insert().Into("users").Values(name="Alice", email="alice@example.com")
|
|
9
|
-
assert q.sql == "INSERT INTO users (name, email) VALUES (
|
|
9
|
+
assert q.sql == "INSERT INTO users (name, email) VALUES (:name, :email)"
|
|
10
10
|
|
|
11
11
|
def test_str_equals_sql(self):
|
|
12
12
|
q = Insert().Into("users").Values(name="Alice")
|
|
@@ -14,17 +14,17 @@ class TestInsertBasic:
|
|
|
14
14
|
|
|
15
15
|
def test_params_single_row(self):
|
|
16
16
|
q = Insert().Into("users").Values(name="Alice", email="alice@example.com")
|
|
17
|
-
assert q.params == ["Alice", "alice@example.com"]
|
|
17
|
+
assert q.params == [{"name": "Alice", "email": "alice@example.com"}]
|
|
18
18
|
|
|
19
19
|
def test_multiple_rows(self):
|
|
20
20
|
q = Insert().Into("users").Values(name="Alice").Values(name="Bob")
|
|
21
|
-
assert q.sql == "INSERT INTO users (name) VALUES (
|
|
22
|
-
assert q.params == ["Alice", "Bob"]
|
|
21
|
+
assert q.sql == "INSERT INTO users (name) VALUES (:name)"
|
|
22
|
+
assert q.params == [{"name": "Alice"}, {"name": "Bob"}]
|
|
23
23
|
|
|
24
24
|
def test_multiple_columns_multiple_rows(self):
|
|
25
25
|
q = Insert().Into("orders").Values(user_id=1, total=99).Values(user_id=2, total=50)
|
|
26
|
-
assert q.sql == "INSERT INTO orders (user_id, total) VALUES (
|
|
27
|
-
assert q.params == [1, 99, 2, 50]
|
|
26
|
+
assert q.sql == "INSERT INTO orders (user_id, total) VALUES (:user_id, :total)"
|
|
27
|
+
assert q.params == [{"user_id": 1, "total": 99}, {"user_id": 2, "total": 50}]
|
|
28
28
|
|
|
29
29
|
def test_method_chaining_returns_insert(self):
|
|
30
30
|
q = Insert()
|
|
@@ -33,23 +33,23 @@ class TestInsertBasic:
|
|
|
33
33
|
|
|
34
34
|
def test_no_table_raises(self):
|
|
35
35
|
with pytest.raises(ValueError):
|
|
36
|
-
Insert().Values(name="Alice").sql
|
|
36
|
+
_ = Insert().Values(name="Alice").sql
|
|
37
37
|
|
|
38
38
|
def test_no_values_raises(self):
|
|
39
39
|
with pytest.raises(ValueError):
|
|
40
|
-
Insert().Into("users").sql
|
|
40
|
+
_ = Insert().Into("users").sql
|
|
41
41
|
|
|
42
42
|
def test_empty_table_raises(self):
|
|
43
43
|
with pytest.raises(ValueError):
|
|
44
|
-
Insert().Into("")
|
|
44
|
+
_ = Insert().Into("")
|
|
45
45
|
|
|
46
46
|
def test_blank_table_raises(self):
|
|
47
47
|
with pytest.raises(ValueError):
|
|
48
|
-
Insert().Into(" ")
|
|
48
|
+
_ = Insert().Into(" ")
|
|
49
49
|
|
|
50
50
|
def test_empty_values_raises(self):
|
|
51
51
|
with pytest.raises(ValueError):
|
|
52
|
-
Insert().Into("users").Values()
|
|
52
|
+
_ = Insert().Into("users").Values()
|
|
53
53
|
|
|
54
54
|
def test_params_empty_before_values(self):
|
|
55
55
|
q = Insert().Into("users")
|
|
@@ -57,4 +57,4 @@ class TestInsertBasic:
|
|
|
57
57
|
|
|
58
58
|
def test_various_value_types(self):
|
|
59
59
|
q = Insert().Into("events").Values(user_id=42, active=True, score=3.14, label=None)
|
|
60
|
-
assert q.params == [42, True, 3.14, None]
|
|
60
|
+
assert q.params == [{"user_id": 42, "active": True, "score": 3.14, "label": None}]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import pytest
|
|
2
1
|
from unittest.mock import MagicMock, patch
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
from sqlqb.pymysql import Select, Insert, Update, Delete, Connection
|
|
3
|
+
import pytest
|
|
6
4
|
from pymysql.cursors import DictCursor
|
|
7
5
|
|
|
6
|
+
from sqlqb import pymysql
|
|
7
|
+
from sqlqb.pymysql import Connection, Delete, Insert, Select, Update, _to_pymysql
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
@pytest.fixture
|
|
10
11
|
def mock_conn():
|
|
@@ -33,9 +34,10 @@ class TestSelect:
|
|
|
33
34
|
|
|
34
35
|
def test_execute_calls_cursor_execute_with_sql_and_params(self, mock_conn):
|
|
35
36
|
conn, cursor = mock_conn
|
|
36
|
-
q = Select(conn, "name").From("users").Where("id =
|
|
37
|
+
q = Select(conn, "name").From("users").Where("id = :id", id=1)
|
|
37
38
|
q.execute()
|
|
38
|
-
|
|
39
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
40
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
39
41
|
|
|
40
42
|
def test_execute_does_not_use_context_manager(self, mock_conn):
|
|
41
43
|
conn, cursor = mock_conn
|
|
@@ -45,20 +47,21 @@ class TestSelect:
|
|
|
45
47
|
def test_fetchone_returns_result(self, mock_conn):
|
|
46
48
|
conn, cursor = mock_conn
|
|
47
49
|
cursor.fetchone.return_value = {"id": 1, "name": "Alice"}
|
|
48
|
-
result = Select(conn, "id", "name").From("users").Where("id =
|
|
50
|
+
result = Select(conn, "id", "name").From("users").Where("id = :id", id=1).fetchone()
|
|
49
51
|
assert result == {"id": 1, "name": "Alice"}
|
|
50
52
|
|
|
51
53
|
def test_fetchone_returns_none_when_no_row(self, mock_conn):
|
|
52
54
|
conn, cursor = mock_conn
|
|
53
55
|
cursor.fetchone.return_value = None
|
|
54
|
-
result = Select(conn, "id").From("users").Where("id =
|
|
56
|
+
result = Select(conn, "id").From("users").Where("id = :id", id=999).fetchone()
|
|
55
57
|
assert result is None
|
|
56
58
|
|
|
57
59
|
def test_fetchone_calls_cursor_execute(self, mock_conn):
|
|
58
60
|
conn, cursor = mock_conn
|
|
59
|
-
q = Select(conn, "name").From("users").Where("id =
|
|
61
|
+
q = Select(conn, "name").From("users").Where("id = :id", id=1)
|
|
60
62
|
q.fetchone()
|
|
61
|
-
|
|
63
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
64
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
62
65
|
|
|
63
66
|
def test_fetchone_uses_context_manager(self, mock_conn):
|
|
64
67
|
conn, cursor = mock_conn
|
|
@@ -90,14 +93,15 @@ class TestSelect:
|
|
|
90
93
|
|
|
91
94
|
def test_multiple_where_params_passed_correctly(self, mock_conn):
|
|
92
95
|
conn, cursor = mock_conn
|
|
93
|
-
q = Select(conn, "name").From("users").Where("age >
|
|
96
|
+
q = Select(conn, "name").From("users").Where("age > :age", age=18).Where("active = :active", active=True)
|
|
94
97
|
q.fetchall()
|
|
95
|
-
|
|
98
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
99
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
96
100
|
|
|
97
101
|
def test_chaining_with_order_and_limit(self, mock_conn):
|
|
98
102
|
conn, cursor = mock_conn
|
|
99
103
|
cursor.fetchall.return_value = [{"name": "Alice"}]
|
|
100
|
-
result = Select(conn, "name").From("users").Where("age >=
|
|
104
|
+
result = Select(conn, "name").From("users").Where("age >= :age", age=18).OrderBy("name").Limit(1).fetchall()
|
|
101
105
|
assert result == [{"name": "Alice"}]
|
|
102
106
|
|
|
103
107
|
|
|
@@ -130,14 +134,15 @@ class TestUpdate:
|
|
|
130
134
|
def test_execute_returns_rowcount(self, mock_conn):
|
|
131
135
|
conn, cursor = mock_conn
|
|
132
136
|
cursor.rowcount = 1
|
|
133
|
-
count = Update(conn, "users").Set(name="Alice").Where("id =
|
|
137
|
+
count = Update(conn, "users").Set(name="Alice").Where("id = :id", id=1).execute()
|
|
134
138
|
assert count == 1
|
|
135
139
|
|
|
136
140
|
def test_execute_calls_cursor_execute(self, mock_conn):
|
|
137
141
|
conn, cursor = mock_conn
|
|
138
|
-
q = Update(conn, "users").Set(name="Alice").Where("id =
|
|
142
|
+
q = Update(conn, "users").Set(name="Alice").Where("id = :id", id=1)
|
|
139
143
|
q.execute()
|
|
140
|
-
|
|
144
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
145
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
141
146
|
|
|
142
147
|
def test_execute_uses_context_manager(self, mock_conn):
|
|
143
148
|
conn, cursor = mock_conn
|
|
@@ -153,9 +158,10 @@ class TestUpdate:
|
|
|
153
158
|
def test_execute_multiple_set_columns(self, mock_conn):
|
|
154
159
|
conn, cursor = mock_conn
|
|
155
160
|
cursor.rowcount = 1
|
|
156
|
-
q = Update(conn, "users").Set(name="Bob", age=20).Where("id =
|
|
161
|
+
q = Update(conn, "users").Set(name="Bob", age=20).Where("id = :id", id=2)
|
|
157
162
|
count = q.execute()
|
|
158
|
-
|
|
163
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
164
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
159
165
|
assert count == 1
|
|
160
166
|
|
|
161
167
|
|
|
@@ -163,18 +169,19 @@ class TestDelete:
|
|
|
163
169
|
def test_execute_returns_rowcount(self, mock_conn):
|
|
164
170
|
conn, cursor = mock_conn
|
|
165
171
|
cursor.rowcount = 1
|
|
166
|
-
count = Delete(conn).From("users").Where("id =
|
|
172
|
+
count = Delete(conn).From("users").Where("id = :id", id=1).execute()
|
|
167
173
|
assert count == 1
|
|
168
174
|
|
|
169
175
|
def test_execute_calls_cursor_execute(self, mock_conn):
|
|
170
176
|
conn, cursor = mock_conn
|
|
171
|
-
q = Delete(conn).From("users").Where("id =
|
|
177
|
+
q = Delete(conn).From("users").Where("id = :id", id=1)
|
|
172
178
|
q.execute()
|
|
173
|
-
|
|
179
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
180
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
174
181
|
|
|
175
182
|
def test_execute_uses_context_manager(self, mock_conn):
|
|
176
183
|
conn, cursor = mock_conn
|
|
177
|
-
Delete(conn).From("users").Where("id =
|
|
184
|
+
Delete(conn).From("users").Where("id = :id", id=1).execute()
|
|
178
185
|
cursor.__enter__.assert_called_once()
|
|
179
186
|
|
|
180
187
|
def test_execute_no_where_returns_all_rowcount(self, mock_conn):
|
|
@@ -183,13 +190,20 @@ class TestDelete:
|
|
|
183
190
|
count = Delete(conn).From("users").execute()
|
|
184
191
|
assert count == 3
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
|
|
194
|
+
class TestToPymysql:
|
|
195
|
+
def test_dict_params_converts_placeholders(self):
|
|
196
|
+
sql, params = _to_pymysql(
|
|
197
|
+
"SELECT * FROM t WHERE id = :id AND name = :name",
|
|
198
|
+
{"id": 1, "name": "Alice"},
|
|
199
|
+
)
|
|
200
|
+
assert sql == "SELECT * FROM t WHERE id = %(id)s AND name = %(name)s"
|
|
201
|
+
assert params == {"id": 1, "name": "Alice"}
|
|
202
|
+
|
|
203
|
+
def test_dict_params_single_placeholder(self):
|
|
204
|
+
sql, params = _to_pymysql("DELETE FROM t WHERE id = :id", {"id": 42})
|
|
205
|
+
assert sql == "DELETE FROM t WHERE id = %(id)s"
|
|
206
|
+
assert params == {"id": 42}
|
|
193
207
|
|
|
194
208
|
|
|
195
209
|
class TestConnection:
|