sqlqb 0.1.4__tar.gz → 0.2.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.
- sqlqb-0.2.0/.claude/settings.local.json +7 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/PKG-INFO +1 -1
- {sqlqb-0.1.4 → sqlqb-0.2.0}/pyproject.toml +1 -1
- sqlqb-0.2.0/src/sqlqb/delete.py +36 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/src/sqlqb/insert.py +8 -10
- {sqlqb-0.1.4 → sqlqb-0.2.0}/src/sqlqb/select.py +13 -33
- {sqlqb-0.1.4 → sqlqb-0.2.0}/src/sqlqb/sqlalchemy/__init__.py +4 -22
- {sqlqb-0.1.4 → sqlqb-0.2.0}/src/sqlqb/sqlite3/__init__.py +6 -1
- sqlqb-0.2.0/src/sqlqb/update.py +43 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/tests/test_delete.py +13 -32
- {sqlqb-0.1.4 → sqlqb-0.2.0}/tests/test_insert.py +7 -7
- {sqlqb-0.1.4 → sqlqb-0.2.0}/tests/test_pymysql.py +39 -26
- {sqlqb-0.1.4 → sqlqb-0.2.0}/tests/test_select.py +43 -22
- {sqlqb-0.1.4 → sqlqb-0.2.0}/tests/test_sqlalchemy.py +16 -16
- {sqlqb-0.1.4 → sqlqb-0.2.0}/tests/test_sqlite3.py +28 -23
- {sqlqb-0.1.4 → sqlqb-0.2.0}/tests/test_update.py +21 -32
- {sqlqb-0.1.4 → sqlqb-0.2.0}/uv.lock +1 -1
- sqlqb-0.1.4/src/sqlqb/delete.py +0 -57
- sqlqb-0.1.4/src/sqlqb/update.py +0 -69
- {sqlqb-0.1.4 → sqlqb-0.2.0}/.envrc +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/.gitignore +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/.pythonrc.py +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/Makefile +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/README.md +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/data.db +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/src/sqlqb/__init__.py +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/src/sqlqb/pymysql/__init__.py +0 -0
- {sqlqb-0.1.4 → sqlqb-0.2.0}/tests/__init__.py +0 -0
|
@@ -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))
|
|
@@ -9,34 +9,19 @@ from sqlqb import Update as _Update
|
|
|
9
9
|
from sqlqb import Delete as _Delete
|
|
10
10
|
|
|
11
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
|
-
|
|
23
|
-
|
|
24
12
|
class Select(_Select):
|
|
25
13
|
def __init__(self, connection: _Connection, *args):
|
|
26
14
|
super().__init__(*args)
|
|
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):
|
|
@@ -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
|
|
@@ -18,64 +18,45 @@ class TestDeleteBasic:
|
|
|
18
18
|
|
|
19
19
|
def test_empty_table_raises(self):
|
|
20
20
|
with pytest.raises(ValueError):
|
|
21
|
-
Delete().From("")
|
|
21
|
+
Delete().From("").sql
|
|
22
22
|
|
|
23
23
|
def test_blank_table_raises(self):
|
|
24
24
|
with pytest.raises(ValueError):
|
|
25
|
-
Delete().From(" ")
|
|
25
|
+
Delete().From(" ").sql
|
|
26
26
|
|
|
27
27
|
def test_params_empty_by_default(self):
|
|
28
28
|
q = Delete().From("users")
|
|
29
|
-
assert q.params ==
|
|
29
|
+
assert q.params == {}
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class TestDeleteWhere:
|
|
33
33
|
def test_single_where(self):
|
|
34
|
-
q = Delete().From("users").Where("id =
|
|
35
|
-
assert q.sql == "DELETE FROM users WHERE id =
|
|
36
|
-
assert q.params ==
|
|
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
37
|
|
|
38
38
|
def test_multiple_wheres_joined_with_and(self):
|
|
39
|
-
q = Delete().From("users").Where("active =
|
|
40
|
-
assert q.sql == "DELETE FROM users WHERE active =
|
|
41
|
-
assert q.params ==
|
|
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
42
|
|
|
43
43
|
def test_where_no_params(self):
|
|
44
44
|
q = Delete().From("sessions").Where("expired_at IS NOT NULL")
|
|
45
45
|
assert q.sql == "DELETE FROM sessions WHERE expired_at IS NOT NULL"
|
|
46
|
-
assert q.params ==
|
|
47
|
-
|
|
48
|
-
def test_where_multiple_params(self):
|
|
49
|
-
q = Delete().From("t").Where("a = ? AND b = ?", 1, 2)
|
|
50
|
-
assert q.params == [1, 2]
|
|
46
|
+
assert q.params == {}
|
|
51
47
|
|
|
52
48
|
def test_no_where_absent(self):
|
|
53
49
|
q = Delete().From("users")
|
|
54
50
|
assert "WHERE" not in q.sql
|
|
55
51
|
|
|
56
52
|
|
|
57
|
-
class TestDeleteLimit:
|
|
58
|
-
def test_limit(self):
|
|
59
|
-
q = Delete().From("logs").Limit(100)
|
|
60
|
-
assert "LIMIT 100" in q.sql
|
|
61
|
-
|
|
62
|
-
def test_no_order_by_absent(self):
|
|
63
|
-
q = Delete().From("users")
|
|
64
|
-
assert "ORDER BY" not in q.sql
|
|
65
|
-
|
|
66
|
-
def test_no_limit_absent(self):
|
|
67
|
-
q = Delete().From("users")
|
|
68
|
-
assert "LIMIT" not in q.sql
|
|
69
|
-
|
|
70
|
-
|
|
71
53
|
class TestDeleteChaining:
|
|
72
54
|
def test_method_chaining_returns_delete(self):
|
|
73
55
|
q = Delete()
|
|
74
56
|
assert q.From("t") is q
|
|
75
57
|
assert q.Where("1=1") is q
|
|
76
|
-
assert q.Limit(5) is q
|
|
77
58
|
|
|
78
59
|
def test_full_query(self):
|
|
79
|
-
q = Delete().From("logs").Where("level =
|
|
80
|
-
assert q.sql == "DELETE FROM logs WHERE level =
|
|
81
|
-
assert q.params ==
|
|
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()
|
|
@@ -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}]
|
|
@@ -2,7 +2,7 @@ import pytest
|
|
|
2
2
|
from unittest.mock import MagicMock, patch
|
|
3
3
|
|
|
4
4
|
from sqlqb import pymysql
|
|
5
|
-
from sqlqb.pymysql import Select, Insert, Update, Delete, Connection
|
|
5
|
+
from sqlqb.pymysql import Select, Insert, Update, Delete, Connection, _to_pymysql
|
|
6
6
|
from pymysql.cursors import DictCursor
|
|
7
7
|
|
|
8
8
|
|
|
@@ -33,9 +33,10 @@ class TestSelect:
|
|
|
33
33
|
|
|
34
34
|
def test_execute_calls_cursor_execute_with_sql_and_params(self, mock_conn):
|
|
35
35
|
conn, cursor = mock_conn
|
|
36
|
-
q = Select(conn, "name").From("users").Where("id =
|
|
36
|
+
q = Select(conn, "name").From("users").Where("id = :id", id=1)
|
|
37
37
|
q.execute()
|
|
38
|
-
|
|
38
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
39
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
39
40
|
|
|
40
41
|
def test_execute_does_not_use_context_manager(self, mock_conn):
|
|
41
42
|
conn, cursor = mock_conn
|
|
@@ -45,20 +46,21 @@ class TestSelect:
|
|
|
45
46
|
def test_fetchone_returns_result(self, mock_conn):
|
|
46
47
|
conn, cursor = mock_conn
|
|
47
48
|
cursor.fetchone.return_value = {"id": 1, "name": "Alice"}
|
|
48
|
-
result = Select(conn, "id", "name").From("users").Where("id =
|
|
49
|
+
result = Select(conn, "id", "name").From("users").Where("id = :id", id=1).fetchone()
|
|
49
50
|
assert result == {"id": 1, "name": "Alice"}
|
|
50
51
|
|
|
51
52
|
def test_fetchone_returns_none_when_no_row(self, mock_conn):
|
|
52
53
|
conn, cursor = mock_conn
|
|
53
54
|
cursor.fetchone.return_value = None
|
|
54
|
-
result = Select(conn, "id").From("users").Where("id =
|
|
55
|
+
result = Select(conn, "id").From("users").Where("id = :id", id=999).fetchone()
|
|
55
56
|
assert result is None
|
|
56
57
|
|
|
57
58
|
def test_fetchone_calls_cursor_execute(self, mock_conn):
|
|
58
59
|
conn, cursor = mock_conn
|
|
59
|
-
q = Select(conn, "name").From("users").Where("id =
|
|
60
|
+
q = Select(conn, "name").From("users").Where("id = :id", id=1)
|
|
60
61
|
q.fetchone()
|
|
61
|
-
|
|
62
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
63
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
62
64
|
|
|
63
65
|
def test_fetchone_uses_context_manager(self, mock_conn):
|
|
64
66
|
conn, cursor = mock_conn
|
|
@@ -90,14 +92,15 @@ class TestSelect:
|
|
|
90
92
|
|
|
91
93
|
def test_multiple_where_params_passed_correctly(self, mock_conn):
|
|
92
94
|
conn, cursor = mock_conn
|
|
93
|
-
q = Select(conn, "name").From("users").Where("age >
|
|
95
|
+
q = Select(conn, "name").From("users").Where("age > :age", age=18).Where("active = :active", active=True)
|
|
94
96
|
q.fetchall()
|
|
95
|
-
|
|
97
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
98
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
96
99
|
|
|
97
100
|
def test_chaining_with_order_and_limit(self, mock_conn):
|
|
98
101
|
conn, cursor = mock_conn
|
|
99
102
|
cursor.fetchall.return_value = [{"name": "Alice"}]
|
|
100
|
-
result = Select(conn, "name").From("users").Where("age >=
|
|
103
|
+
result = Select(conn, "name").From("users").Where("age >= :age", age=18).OrderBy("name").Limit(1).fetchall()
|
|
101
104
|
assert result == [{"name": "Alice"}]
|
|
102
105
|
|
|
103
106
|
|
|
@@ -130,14 +133,15 @@ class TestUpdate:
|
|
|
130
133
|
def test_execute_returns_rowcount(self, mock_conn):
|
|
131
134
|
conn, cursor = mock_conn
|
|
132
135
|
cursor.rowcount = 1
|
|
133
|
-
count = Update(conn, "users").Set(name="Alice").Where("id =
|
|
136
|
+
count = Update(conn, "users").Set(name="Alice").Where("id = :id", id=1).execute()
|
|
134
137
|
assert count == 1
|
|
135
138
|
|
|
136
139
|
def test_execute_calls_cursor_execute(self, mock_conn):
|
|
137
140
|
conn, cursor = mock_conn
|
|
138
|
-
q = Update(conn, "users").Set(name="Alice").Where("id =
|
|
141
|
+
q = Update(conn, "users").Set(name="Alice").Where("id = :id", id=1)
|
|
139
142
|
q.execute()
|
|
140
|
-
|
|
143
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
144
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
141
145
|
|
|
142
146
|
def test_execute_uses_context_manager(self, mock_conn):
|
|
143
147
|
conn, cursor = mock_conn
|
|
@@ -153,9 +157,10 @@ class TestUpdate:
|
|
|
153
157
|
def test_execute_multiple_set_columns(self, mock_conn):
|
|
154
158
|
conn, cursor = mock_conn
|
|
155
159
|
cursor.rowcount = 1
|
|
156
|
-
q = Update(conn, "users").Set(name="Bob", age=20).Where("id =
|
|
160
|
+
q = Update(conn, "users").Set(name="Bob", age=20).Where("id = :id", id=2)
|
|
157
161
|
count = q.execute()
|
|
158
|
-
|
|
162
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
163
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
159
164
|
assert count == 1
|
|
160
165
|
|
|
161
166
|
|
|
@@ -163,18 +168,19 @@ class TestDelete:
|
|
|
163
168
|
def test_execute_returns_rowcount(self, mock_conn):
|
|
164
169
|
conn, cursor = mock_conn
|
|
165
170
|
cursor.rowcount = 1
|
|
166
|
-
count = Delete(conn).From("users").Where("id =
|
|
171
|
+
count = Delete(conn).From("users").Where("id = :id", id=1).execute()
|
|
167
172
|
assert count == 1
|
|
168
173
|
|
|
169
174
|
def test_execute_calls_cursor_execute(self, mock_conn):
|
|
170
175
|
conn, cursor = mock_conn
|
|
171
|
-
q = Delete(conn).From("users").Where("id =
|
|
176
|
+
q = Delete(conn).From("users").Where("id = :id", id=1)
|
|
172
177
|
q.execute()
|
|
173
|
-
|
|
178
|
+
expected_sql, expected_params = _to_pymysql(q.sql, q.params)
|
|
179
|
+
cursor.execute.assert_called_once_with(expected_sql, expected_params)
|
|
174
180
|
|
|
175
181
|
def test_execute_uses_context_manager(self, mock_conn):
|
|
176
182
|
conn, cursor = mock_conn
|
|
177
|
-
Delete(conn).From("users").Where("id =
|
|
183
|
+
Delete(conn).From("users").Where("id = :id", id=1).execute()
|
|
178
184
|
cursor.__enter__.assert_called_once()
|
|
179
185
|
|
|
180
186
|
def test_execute_no_where_returns_all_rowcount(self, mock_conn):
|
|
@@ -183,13 +189,20 @@ class TestDelete:
|
|
|
183
189
|
count = Delete(conn).From("users").execute()
|
|
184
190
|
assert count == 3
|
|
185
191
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
|
|
193
|
+
class TestToPymysql:
|
|
194
|
+
def test_dict_params_converts_placeholders(self):
|
|
195
|
+
sql, params = _to_pymysql(
|
|
196
|
+
"SELECT * FROM t WHERE id = :id AND name = :name",
|
|
197
|
+
{"id": 1, "name": "Alice"},
|
|
198
|
+
)
|
|
199
|
+
assert sql == "SELECT * FROM t WHERE id = %(id)s AND name = %(name)s"
|
|
200
|
+
assert params == {"id": 1, "name": "Alice"}
|
|
201
|
+
|
|
202
|
+
def test_dict_params_single_placeholder(self):
|
|
203
|
+
sql, params = _to_pymysql("DELETE FROM t WHERE id = :id", {"id": 42})
|
|
204
|
+
assert sql == "DELETE FROM t WHERE id = %(id)s"
|
|
205
|
+
assert params == {"id": 42}
|
|
193
206
|
|
|
194
207
|
|
|
195
208
|
class TestConnection:
|
|
@@ -32,6 +32,10 @@ class TestSelectBasic:
|
|
|
32
32
|
q = Select("id").From("orders")
|
|
33
33
|
assert "FROM orders" in q.sql
|
|
34
34
|
|
|
35
|
+
def test_table_empty_raises(self):
|
|
36
|
+
with pytest.raises(ValueError):
|
|
37
|
+
Select("1").sql
|
|
38
|
+
|
|
35
39
|
def test_columns_empty_raises(self):
|
|
36
40
|
with pytest.raises(ValueError):
|
|
37
41
|
Select().Columns()
|
|
@@ -58,23 +62,19 @@ class TestSelectBasic:
|
|
|
58
62
|
|
|
59
63
|
class TestWhere:
|
|
60
64
|
def test_single_where(self):
|
|
61
|
-
q = Select("id").From("users").Where("id =
|
|
62
|
-
assert q.sql == "SELECT id FROM users WHERE id =
|
|
63
|
-
assert q.params ==
|
|
65
|
+
q = Select("id").From("users").Where("id = :id", id=1)
|
|
66
|
+
assert q.sql == "SELECT id FROM users WHERE id = :id"
|
|
67
|
+
assert q.params == {"id": 1}
|
|
64
68
|
|
|
65
69
|
def test_multiple_wheres_joined_with_and(self):
|
|
66
|
-
q = Select("id").From("users").Where("active =
|
|
67
|
-
assert q.sql == "SELECT id FROM users WHERE active =
|
|
68
|
-
assert q.params ==
|
|
70
|
+
q = Select("id").From("users").Where("active = :active", active=True).Where("role = :role", role="admin")
|
|
71
|
+
assert q.sql == "SELECT id FROM users WHERE active = :active AND role = :role"
|
|
72
|
+
assert q.params == {"active": True, "role": "admin"}
|
|
69
73
|
|
|
70
74
|
def test_where_no_params(self):
|
|
71
75
|
q = Select("id").From("users").Where("deleted_at IS NULL")
|
|
72
76
|
assert "WHERE deleted_at IS NULL" in q.sql
|
|
73
|
-
assert q.params ==
|
|
74
|
-
|
|
75
|
-
def test_where_multiple_params(self):
|
|
76
|
-
q = Select().From("t").Where("a = ? AND b = ?", 1, 2)
|
|
77
|
-
assert q.params == [1, 2]
|
|
77
|
+
assert q.params == {}
|
|
78
78
|
|
|
79
79
|
def test_no_where_clause_absent(self):
|
|
80
80
|
q = Select("id").From("users")
|
|
@@ -92,12 +92,17 @@ class TestJoin:
|
|
|
92
92
|
assert "JOIN items i ON i.order_id = o.id" in q.sql
|
|
93
93
|
|
|
94
94
|
def test_join_with_params(self):
|
|
95
|
-
q = Select("u.id").From("users u").Join("JOIN orders o ON o.user_id = u.id AND o.status =
|
|
96
|
-
assert q.params ==
|
|
97
|
-
|
|
98
|
-
def
|
|
99
|
-
q =
|
|
100
|
-
|
|
95
|
+
q = Select("u.id").From("users u").Join("JOIN orders o ON o.user_id = u.id AND o.status = :status", status="active")
|
|
96
|
+
assert q.params == {"status": "active"}
|
|
97
|
+
|
|
98
|
+
def test_join_and_where_params(self):
|
|
99
|
+
q = (
|
|
100
|
+
Select("u.id")
|
|
101
|
+
.From("users u")
|
|
102
|
+
.Join("JOIN orders o ON o.user_id = u.id AND o.type = :type", type="sale")
|
|
103
|
+
.Where("u.active = :active", active=True)
|
|
104
|
+
)
|
|
105
|
+
assert q.params == {"type": "sale", "active": True}
|
|
101
106
|
|
|
102
107
|
def test_no_join_absent(self):
|
|
103
108
|
q = Select("id").From("users")
|
|
@@ -178,17 +183,33 @@ class TestLimitOffset:
|
|
|
178
183
|
class TestParams:
|
|
179
184
|
def test_params_empty_by_default(self):
|
|
180
185
|
q = Select("id").From("users")
|
|
181
|
-
assert q.params ==
|
|
186
|
+
assert q.params == {}
|
|
182
187
|
|
|
183
188
|
def test_params_accumulate_across_joins_and_wheres(self):
|
|
184
|
-
q =
|
|
185
|
-
|
|
189
|
+
q = (
|
|
190
|
+
Select("id")
|
|
191
|
+
.From("users")
|
|
192
|
+
.Join("JOIN t ON t.id = users.t_id AND t.x = :x", x=42)
|
|
193
|
+
.Where("active = :active", active=True)
|
|
194
|
+
.Where("role = :role", role="admin")
|
|
195
|
+
)
|
|
196
|
+
assert q.params == {"x": 42, "active": True, "role": "admin"}
|
|
186
197
|
|
|
187
198
|
|
|
188
199
|
class TestClauseOrdering:
|
|
189
200
|
def test_full_query_clause_order(self):
|
|
190
|
-
sql =
|
|
191
|
-
|
|
201
|
+
sql = (
|
|
202
|
+
Select("u.id", "u.name")
|
|
203
|
+
.From("users u")
|
|
204
|
+
.Join("JOIN orders o ON o.user_id = u.id")
|
|
205
|
+
.Where("u.active = :active", active=True)
|
|
206
|
+
.GroupBy("u.id", "u.name")
|
|
207
|
+
.OrderBy("u.name")
|
|
208
|
+
.Limit(25)
|
|
209
|
+
.Offset(50)
|
|
210
|
+
.sql
|
|
211
|
+
)
|
|
212
|
+
assert sql == "SELECT u.id, u.name FROM users u JOIN orders o ON o.user_id = u.id WHERE u.active = :active GROUP BY u.id, u.name ORDER BY u.name ASC LIMIT 25 OFFSET 50"
|
|
192
213
|
|
|
193
214
|
def test_group_by_before_order_by(self):
|
|
194
215
|
sql = Select("a").From("t").OrderBy("a").GroupBy("a").sql
|
|
@@ -107,23 +107,23 @@ class TestSelect:
|
|
|
107
107
|
assert rows == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Carol"}]
|
|
108
108
|
|
|
109
109
|
def test_fetchone(self, conn):
|
|
110
|
-
row = conn.Select("name").From("users").Where("id =
|
|
110
|
+
row = conn.Select("name").From("users").Where("id = :id", id=1).fetchone()
|
|
111
111
|
assert row == {"name": "Alice"}
|
|
112
112
|
|
|
113
113
|
def test_fetchone_returns_none_when_no_row(self, conn):
|
|
114
|
-
row = conn.Select("name").From("users").Where("id =
|
|
114
|
+
row = conn.Select("name").From("users").Where("id = :id", id=999).fetchone()
|
|
115
115
|
assert row is None
|
|
116
116
|
|
|
117
117
|
def test_fetchall_returns_empty_list(self, conn):
|
|
118
|
-
rows = conn.Select("name").From("users").Where("id =
|
|
118
|
+
rows = conn.Select("name").From("users").Where("id = :id", id=999).fetchall()
|
|
119
119
|
assert rows == []
|
|
120
120
|
|
|
121
121
|
def test_where_single_param(self, conn):
|
|
122
|
-
rows = conn.Select("name").From("users").Where("age >=
|
|
122
|
+
rows = conn.Select("name").From("users").Where("age >= :age", age=18).fetchall()
|
|
123
123
|
assert rows == [{"name": "Alice"}, {"name": "Carol"}]
|
|
124
124
|
|
|
125
125
|
def test_where_multiple_params(self, conn):
|
|
126
|
-
rows = conn.Select("name").From("users").Where("age >=
|
|
126
|
+
rows = conn.Select("name").From("users").Where("age >= :min_age", min_age=18).Where("age <= :max_age", max_age=29).fetchall()
|
|
127
127
|
assert rows == [{"name": "Carol"}]
|
|
128
128
|
|
|
129
129
|
def test_order_by_asc(self, conn):
|
|
@@ -148,7 +148,7 @@ class TestSelect:
|
|
|
148
148
|
assert type(rows[0]) is dict
|
|
149
149
|
|
|
150
150
|
def test_fetchone_returns_plain_dict(self, conn):
|
|
151
|
-
row = conn.Select("id").From("users").Where("id =
|
|
151
|
+
row = conn.Select("id").From("users").Where("id = :id", id=1).fetchone()
|
|
152
152
|
assert type(row) is dict
|
|
153
153
|
|
|
154
154
|
|
|
@@ -164,7 +164,7 @@ class TestInsert:
|
|
|
164
164
|
|
|
165
165
|
def test_inserted_row_is_queryable(self, conn):
|
|
166
166
|
conn.Insert().Into("users").Values(id=4, name="Dave", age=40).execute()
|
|
167
|
-
row = conn.Select("name").From("users").Where("id =
|
|
167
|
+
row = conn.Select("name").From("users").Where("id = :id", id=4).fetchone()
|
|
168
168
|
assert row == {"name": "Dave"}
|
|
169
169
|
|
|
170
170
|
def test_insert_multiple_rows(self, conn):
|
|
@@ -179,17 +179,17 @@ class TestInsert:
|
|
|
179
179
|
|
|
180
180
|
class TestUpdate:
|
|
181
181
|
def test_execute_returns_rowcount(self, conn):
|
|
182
|
-
count = conn.Update("users").Set(name="Alicia").Where("id =
|
|
182
|
+
count = conn.Update("users").Set(name="Alicia").Where("id = :id", id=1).execute()
|
|
183
183
|
assert count == 1
|
|
184
184
|
|
|
185
185
|
def test_update_is_reflected(self, conn):
|
|
186
|
-
conn.Update("users").Set(name="Alicia").Where("id =
|
|
187
|
-
row = conn.Select("name").From("users").Where("id =
|
|
186
|
+
conn.Update("users").Set(name="Alicia").Where("id = :id", id=1).execute()
|
|
187
|
+
row = conn.Select("name").From("users").Where("id = :id", id=1).fetchone()
|
|
188
188
|
assert row == {"name": "Alicia"}
|
|
189
189
|
|
|
190
190
|
def test_update_multiple_columns(self, conn):
|
|
191
|
-
conn.Update("users").Set(name="Bobby", age=18).Where("id =
|
|
192
|
-
row = conn.Select("name", "age").From("users").Where("id =
|
|
191
|
+
conn.Update("users").Set(name="Bobby", age=18).Where("id = :id", id=2).execute()
|
|
192
|
+
row = conn.Select("name", "age").From("users").Where("id = :id", id=2).fetchone()
|
|
193
193
|
assert row == {"name": "Bobby", "age": 18}
|
|
194
194
|
|
|
195
195
|
def test_update_no_where_affects_all(self, conn):
|
|
@@ -197,7 +197,7 @@ class TestUpdate:
|
|
|
197
197
|
assert count == 3
|
|
198
198
|
|
|
199
199
|
def test_update_affects_correct_rows_only(self, conn):
|
|
200
|
-
conn.Update("users").Set(age=99).Where("id =
|
|
200
|
+
conn.Update("users").Set(age=99).Where("id = :id", id=1).execute()
|
|
201
201
|
rows = conn.Select("id", "age").From("users").fetchall()
|
|
202
202
|
assert rows == [{"id": 1, "age": 99}, {"id": 2, "age": 17}, {"id": 3, "age": 25}]
|
|
203
203
|
|
|
@@ -210,13 +210,13 @@ class TestUpdate:
|
|
|
210
210
|
class TestDelete:
|
|
211
211
|
def test_execute_returns_rowcount(self, conn):
|
|
212
212
|
conn.Insert().Into("users").Values(id=4, name="Temp", age=1).execute()
|
|
213
|
-
count = conn.Delete().From("users").Where("id =
|
|
213
|
+
count = conn.Delete().From("users").Where("id = :id", id=4).execute()
|
|
214
214
|
assert count == 1
|
|
215
215
|
|
|
216
216
|
def test_deleted_row_is_gone(self, conn):
|
|
217
217
|
conn.Insert().Into("users").Values(id=4, name="Temp", age=1).execute()
|
|
218
|
-
conn.Delete().From("users").Where("id =
|
|
219
|
-
row = conn.Select("*").From("users").Where("id =
|
|
218
|
+
conn.Delete().From("users").Where("id = :id", id=4).execute()
|
|
219
|
+
row = conn.Select("*").From("users").Where("id = :id", id=4).fetchone()
|
|
220
220
|
assert row is None
|
|
221
221
|
|
|
222
222
|
def test_delete_no_where_clears_table(self, conn):
|
|
@@ -7,7 +7,10 @@ from sqlqb import sqlite3
|
|
|
7
7
|
def conn():
|
|
8
8
|
c = sqlite3.connect(":memory:")
|
|
9
9
|
c.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
|
|
10
|
-
c.executemany(
|
|
10
|
+
c.executemany(
|
|
11
|
+
"INSERT INTO users VALUES (:id, :name, :age)",
|
|
12
|
+
[{"id": 1, "name": "Alice", "age": 30}, {"id": 2, "name": "Bob", "age": 17}, {"id": 3, "name": "Carol", "age": 25}],
|
|
13
|
+
)
|
|
11
14
|
c.commit()
|
|
12
15
|
return c
|
|
13
16
|
|
|
@@ -18,11 +21,11 @@ class TestSelect:
|
|
|
18
21
|
assert rows == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Carol"}]
|
|
19
22
|
|
|
20
23
|
def test_fetchone(self, conn):
|
|
21
|
-
row = conn.Select("name").From("users").Where("id =
|
|
24
|
+
row = conn.Select("name").From("users").Where("id = :id", id=1).fetchone()
|
|
22
25
|
assert row == {"name": "Alice"}
|
|
23
26
|
|
|
24
27
|
def test_where_with_param(self, conn):
|
|
25
|
-
rows = conn.Select("name").From("users").Where("age >=
|
|
28
|
+
rows = conn.Select("name").From("users").Where("age >= :age", age=18).fetchall()
|
|
26
29
|
assert rows == [{"name": "Alice"}, {"name": "Carol"}]
|
|
27
30
|
|
|
28
31
|
def test_execute_returns_cursor(self, conn):
|
|
@@ -30,7 +33,7 @@ class TestSelect:
|
|
|
30
33
|
assert cursor.fetchone() == {"id": 1, "name": "Alice", "age": 30}
|
|
31
34
|
|
|
32
35
|
def test_chaining(self, conn):
|
|
33
|
-
rows = conn.Select("name").From("users").Where("age >=
|
|
36
|
+
rows = conn.Select("name").From("users").Where("age >= :age", age=18).OrderBy("name").fetchall()
|
|
34
37
|
assert rows == [{"name": "Alice"}, {"name": "Carol"}]
|
|
35
38
|
|
|
36
39
|
def test_limit(self, conn):
|
|
@@ -38,14 +41,16 @@ class TestSelect:
|
|
|
38
41
|
assert rows == [{"id": 1}, {"id": 2}]
|
|
39
42
|
|
|
40
43
|
|
|
44
|
+
|
|
45
|
+
|
|
41
46
|
class TestInsert:
|
|
42
47
|
def test_insert_returns_rowcount(self, conn):
|
|
43
48
|
count = conn.Insert().Into("users").Values(id=4, name="Dave", age=40).execute()
|
|
44
49
|
assert count == 1
|
|
45
50
|
|
|
46
51
|
def test_insert_row_is_queryable(self, conn):
|
|
47
|
-
conn.Insert().Into("users").Values(id=4, name="Dave", age=40).
|
|
48
|
-
row = conn.Select("name").From("users").Where("id =
|
|
52
|
+
conn.Insert().Into("users").Values(id=4, name="Dave", age=40).executemany()
|
|
53
|
+
row = conn.Select("name").From("users").Where("id = :id", id=4).fetchone()
|
|
49
54
|
assert row == {"name": "Dave"}
|
|
50
55
|
|
|
51
56
|
def test_insert_multiple_rows(self, conn):
|
|
@@ -54,27 +59,34 @@ class TestInsert:
|
|
|
54
59
|
|
|
55
60
|
def test_insert_multiple_rows_are_queryable(self, conn):
|
|
56
61
|
conn.Insert().Into("users").Values(id=4, name="Dave", age=40).Values(id=5, name="Eve", age=22).execute()
|
|
57
|
-
rows = conn.Select("name").From("users").Where("id =
|
|
62
|
+
rows = conn.Select("name").From("users").Where("id = :id", id=4).fetchall()
|
|
58
63
|
assert rows == [{"name": "Dave"}]
|
|
59
64
|
|
|
65
|
+
def test_executemany_all_rows_persisted(self, conn):
|
|
66
|
+
conn.Insert().Into("users").Values(id=4, name="Dave", age=40).Values(id=5, name="Eve", age=22).Values(id=6, name="Frank", age=35).execute()
|
|
67
|
+
rows = conn.Select("id", "name", "age").From("users").OrderBy("id").fetchall()
|
|
68
|
+
assert {"id": 4, "name": "Dave", "age": 40} in rows
|
|
69
|
+
assert {"id": 5, "name": "Eve", "age": 22} in rows
|
|
70
|
+
assert {"id": 6, "name": "Frank", "age": 35} in rows
|
|
71
|
+
|
|
60
72
|
|
|
61
73
|
class TestUpdate:
|
|
62
74
|
def test_update_returns_rowcount(self, conn):
|
|
63
|
-
count = conn.Update("users").Set(name="Alicia").Where("id =
|
|
75
|
+
count = conn.Update("users").Set(name="Alicia").Where("id = :id", id=1).execute()
|
|
64
76
|
assert count == 1
|
|
65
77
|
|
|
66
78
|
def test_update_is_reflected(self, conn):
|
|
67
|
-
conn.Update("users").Set(name="Alicia").Where("id =
|
|
68
|
-
row = conn.Select("name").From("users").Where("id =
|
|
79
|
+
conn.Update("users").Set(name="Alicia").Where("id = :id", id=1).execute()
|
|
80
|
+
row = conn.Select("name").From("users").Where("id = :id", id=1).fetchone()
|
|
69
81
|
assert row == {"name": "Alicia"}
|
|
70
82
|
|
|
71
83
|
def test_update_multiple_columns(self, conn):
|
|
72
|
-
conn.Update("users").Set(name="Bobby", age=18).Where("id =
|
|
73
|
-
row = conn.Select("name", "age").From("users").Where("id =
|
|
84
|
+
conn.Update("users").Set(name="Bobby", age=18).Where("id = :id", id=2).execute()
|
|
85
|
+
row = conn.Select("name", "age").From("users").Where("id = :id", id=2).fetchone()
|
|
74
86
|
assert row == {"name": "Bobby", "age": 18}
|
|
75
87
|
|
|
76
88
|
def test_update_affects_correct_rows_only(self, conn):
|
|
77
|
-
conn.Update("users").Set(age=99).Where("id =
|
|
89
|
+
conn.Update("users").Set(age=99).Where("id = :id", id=1).execute()
|
|
78
90
|
rows = conn.Select("age").From("users").fetchall()
|
|
79
91
|
assert rows == [{"age": 99}, {"age": 17}, {"age": 25}]
|
|
80
92
|
|
|
@@ -85,12 +97,12 @@ class TestUpdate:
|
|
|
85
97
|
|
|
86
98
|
class TestDelete:
|
|
87
99
|
def test_delete_returns_rowcount(self, conn):
|
|
88
|
-
count = conn.Delete().From("users").Where("id =
|
|
100
|
+
count = conn.Delete().From("users").Where("id = :id", id=1).execute()
|
|
89
101
|
assert count == 1
|
|
90
102
|
|
|
91
103
|
def test_delete_row_is_gone(self, conn):
|
|
92
|
-
conn.Delete().From("users").Where("id =
|
|
93
|
-
row = conn.Select("*").From("users").Where("id =
|
|
104
|
+
conn.Delete().From("users").Where("id = :id", id=1).execute()
|
|
105
|
+
row = conn.Select("*").From("users").Where("id = :id", id=1).fetchone()
|
|
94
106
|
assert row is None
|
|
95
107
|
|
|
96
108
|
def test_delete_no_where_clears_table(self, conn):
|
|
@@ -98,10 +110,3 @@ class TestDelete:
|
|
|
98
110
|
assert count == 3
|
|
99
111
|
rows = conn.Select("*").From("users").fetchall()
|
|
100
112
|
assert rows == []
|
|
101
|
-
|
|
102
|
-
def test_delete_with_limit_not_supported(self, conn):
|
|
103
|
-
# NOTE: sqlite library of python does not support LIMIT in delete queries
|
|
104
|
-
import sqlite3 as stdlib_sqlite3
|
|
105
|
-
|
|
106
|
-
with pytest.raises(stdlib_sqlite3.OperationalError):
|
|
107
|
-
conn.Delete().From("users").Limit(2).execute()
|
|
@@ -6,13 +6,13 @@ from sqlqb import Update
|
|
|
6
6
|
class TestUpdateBasic:
|
|
7
7
|
def test_single_column(self):
|
|
8
8
|
q = Update("users").Set(name="Alice")
|
|
9
|
-
assert q.sql == "UPDATE users SET name =
|
|
10
|
-
assert q.params ==
|
|
9
|
+
assert q.sql == "UPDATE users SET name = :name"
|
|
10
|
+
assert q.params == {"name": "Alice"}
|
|
11
11
|
|
|
12
12
|
def test_multiple_columns(self):
|
|
13
13
|
q = Update("users").Set(name="Alice", active=True)
|
|
14
|
-
assert q.sql == "UPDATE users SET name =
|
|
15
|
-
assert q.params ==
|
|
14
|
+
assert q.sql == "UPDATE users SET name = :name, active = :active"
|
|
15
|
+
assert q.params == {"name": "Alice", "active": True}
|
|
16
16
|
|
|
17
17
|
def test_str_equals_sql(self):
|
|
18
18
|
q = Update("users").Set(name="Alice")
|
|
@@ -36,65 +36,54 @@ class TestUpdateBasic:
|
|
|
36
36
|
|
|
37
37
|
def test_params_empty_before_set(self):
|
|
38
38
|
q = Update("users")
|
|
39
|
-
assert q.params ==
|
|
39
|
+
assert q.params == {}
|
|
40
40
|
|
|
41
41
|
def test_chained_set_merges_columns(self):
|
|
42
42
|
q = Update("users").Set(name="Alice").Set(active=False)
|
|
43
|
-
assert "name =
|
|
44
|
-
assert "active =
|
|
45
|
-
assert q.params ==
|
|
43
|
+
assert "name = :name" in q.sql
|
|
44
|
+
assert "active = :active" in q.sql
|
|
45
|
+
assert q.params == {"name": "Alice", "active": False}
|
|
46
46
|
|
|
47
47
|
def test_chained_set_overwrites_same_column(self):
|
|
48
48
|
q = Update("users").Set(name="Alice").Set(name="Bob")
|
|
49
|
-
assert q.params ==
|
|
49
|
+
assert q.params == {"name": "Bob"}
|
|
50
50
|
|
|
51
51
|
def test_various_value_types(self):
|
|
52
52
|
q = Update("t").Set(x=42, flag=True, score=3.14, label=None)
|
|
53
|
-
assert q.params ==
|
|
53
|
+
assert q.params == {"x": 42, "flag": True, "score": 3.14, "label": None}
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
class TestUpdateWhere:
|
|
57
57
|
def test_single_where(self):
|
|
58
|
-
q = Update("users").Set(active=False).Where("id =
|
|
59
|
-
assert q.sql == "UPDATE users SET active =
|
|
60
|
-
assert q.params ==
|
|
58
|
+
q = Update("users").Set(active=False).Where("id = :id", id=1)
|
|
59
|
+
assert q.sql == "UPDATE users SET active = :active WHERE id = :id"
|
|
60
|
+
assert q.params == {"active": False, "id": 1}
|
|
61
61
|
|
|
62
62
|
def test_multiple_wheres_joined_with_and(self):
|
|
63
|
-
q = Update("users").Set(
|
|
64
|
-
assert "WHERE active =
|
|
65
|
-
assert q.params ==
|
|
63
|
+
q = Update("users").Set(status="inactive").Where("active = :active", active=False).Where("role = :role", role="admin")
|
|
64
|
+
assert "WHERE active = :active AND role = :role" in q.sql
|
|
65
|
+
assert q.params == {"status": "inactive", "active": False, "role": "admin"}
|
|
66
66
|
|
|
67
67
|
def test_where_no_params(self):
|
|
68
68
|
q = Update("sessions").Set(expired=True).Where("token IS NOT NULL")
|
|
69
69
|
assert "WHERE token IS NOT NULL" in q.sql
|
|
70
70
|
|
|
71
71
|
def test_set_params_before_where_params(self):
|
|
72
|
-
q = Update("t").Set(x=1).Where("y =
|
|
73
|
-
assert q.params ==
|
|
72
|
+
q = Update("t").Set(x=1).Where("y = :y", y=2)
|
|
73
|
+
assert q.params == {"x": 1, "y": 2}
|
|
74
74
|
|
|
75
75
|
def test_no_where_absent(self):
|
|
76
76
|
q = Update("users").Set(active=True)
|
|
77
77
|
assert "WHERE" not in q.sql
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
class TestUpdateLimit:
|
|
81
|
-
def test_limit(self):
|
|
82
|
-
q = Update("logs").Set(processed=True).Limit(100)
|
|
83
|
-
assert "LIMIT 100" in q.sql
|
|
84
|
-
|
|
85
|
-
def test_no_limit_absent(self):
|
|
86
|
-
q = Update("users").Set(active=True)
|
|
87
|
-
assert "LIMIT" not in q.sql
|
|
88
|
-
|
|
89
|
-
|
|
90
80
|
class TestUpdateChaining:
|
|
91
81
|
def test_method_chaining_returns_update(self):
|
|
92
82
|
q = Update("t")
|
|
93
83
|
assert q.Set(x=1) is q
|
|
94
84
|
assert q.Where("1=1") is q
|
|
95
|
-
assert q.Limit(5) is q
|
|
96
85
|
|
|
97
86
|
def test_full_query(self):
|
|
98
|
-
q = Update("users").Set(role="admin", active=True).Where("id =
|
|
99
|
-
assert q.sql == "UPDATE users SET role =
|
|
100
|
-
assert q.params ==
|
|
87
|
+
q = Update("users").Set(role="admin", active=True).Where("id = :id", id=42)
|
|
88
|
+
assert q.sql == "UPDATE users SET role = :role, active = :active WHERE id = :id"
|
|
89
|
+
assert q.params == {"role": "admin", "active": True, "id": 42}
|
sqlqb-0.1.4/src/sqlqb/delete.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
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: list = []
|
|
9
|
-
self.__named_params: dict = {}
|
|
10
|
-
self.__limit: int | None = None
|
|
11
|
-
|
|
12
|
-
def From(self, table: str) -> "Delete":
|
|
13
|
-
if not table or not table.strip():
|
|
14
|
-
raise ValueError("error: table name must not be empty")
|
|
15
|
-
self.__table = table
|
|
16
|
-
return self
|
|
17
|
-
|
|
18
|
-
def Where(self, condition: str, *args, **kwargs) -> "Delete":
|
|
19
|
-
if args and kwargs:
|
|
20
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
21
|
-
if args and self.__named_params:
|
|
22
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
23
|
-
if kwargs and self.__params:
|
|
24
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
25
|
-
self.__wheres.append(condition)
|
|
26
|
-
if kwargs:
|
|
27
|
-
self.__named_params.update(kwargs)
|
|
28
|
-
else:
|
|
29
|
-
self.__params += list(args)
|
|
30
|
-
return self
|
|
31
|
-
|
|
32
|
-
def Limit(self, limit: int) -> "Delete":
|
|
33
|
-
self.__limit = limit
|
|
34
|
-
return self
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
def params(self) -> list | dict:
|
|
38
|
-
if self.__named_params:
|
|
39
|
-
return self.__named_params
|
|
40
|
-
return self.__params
|
|
41
|
-
|
|
42
|
-
@property
|
|
43
|
-
def sql(self) -> str:
|
|
44
|
-
if not self.__table:
|
|
45
|
-
raise ValueError("error: no table specified")
|
|
46
|
-
sql = io.StringIO()
|
|
47
|
-
sql.write("DELETE FROM ")
|
|
48
|
-
sql.write(self.__table)
|
|
49
|
-
if self.__wheres:
|
|
50
|
-
sql.write(" WHERE ")
|
|
51
|
-
sql.write(" AND ".join(self.__wheres))
|
|
52
|
-
if self.__limit is not None:
|
|
53
|
-
sql.write(f" LIMIT {self.__limit}")
|
|
54
|
-
return sql.getvalue()
|
|
55
|
-
|
|
56
|
-
def __str__(self) -> str:
|
|
57
|
-
return self.sql
|
sqlqb-0.1.4/src/sqlqb/update.py
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
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.__where_params: list = []
|
|
12
|
-
self.__where_named_params: dict = {}
|
|
13
|
-
self.__limit: int | None = None
|
|
14
|
-
|
|
15
|
-
def Set(self, **kwargs) -> "Update":
|
|
16
|
-
if not kwargs:
|
|
17
|
-
raise ValueError("error: specify at least one column value")
|
|
18
|
-
self.__sets.update(kwargs)
|
|
19
|
-
return self
|
|
20
|
-
|
|
21
|
-
def Where(self, condition: str, *args, **kwargs) -> "Update":
|
|
22
|
-
if args and kwargs:
|
|
23
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
24
|
-
if args and self.__where_named_params:
|
|
25
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
26
|
-
if kwargs and self.__where_params:
|
|
27
|
-
raise ValueError("error: cannot mix positional and named params")
|
|
28
|
-
self.__wheres.append(condition)
|
|
29
|
-
if kwargs:
|
|
30
|
-
self.__where_named_params.update(kwargs)
|
|
31
|
-
else:
|
|
32
|
-
self.__where_params += list(args)
|
|
33
|
-
return self
|
|
34
|
-
|
|
35
|
-
def Limit(self, limit: int) -> "Update":
|
|
36
|
-
self.__limit = limit
|
|
37
|
-
return self
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def params(self) -> list | dict:
|
|
41
|
-
if not self.__sets:
|
|
42
|
-
return []
|
|
43
|
-
if self.__where_named_params:
|
|
44
|
-
named = dict(self.__sets)
|
|
45
|
-
named.update(self.__where_named_params)
|
|
46
|
-
return named
|
|
47
|
-
return list(self.__sets.values()) + self.__where_params
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def sql(self) -> str:
|
|
51
|
-
if not self.__sets:
|
|
52
|
-
raise ValueError("error: no SET values specified")
|
|
53
|
-
sql = io.StringIO()
|
|
54
|
-
sql.write("UPDATE ")
|
|
55
|
-
sql.write(self.__table)
|
|
56
|
-
sql.write(" SET ")
|
|
57
|
-
if self.__where_named_params:
|
|
58
|
-
sql.write(", ".join([f"{col} = :{col}" for col in self.__sets]))
|
|
59
|
-
else:
|
|
60
|
-
sql.write(", ".join([f"{col} = ?" for col in self.__sets]))
|
|
61
|
-
if self.__wheres:
|
|
62
|
-
sql.write(" WHERE ")
|
|
63
|
-
sql.write(" AND ".join(self.__wheres))
|
|
64
|
-
if self.__limit is not None:
|
|
65
|
-
sql.write(f" LIMIT {self.__limit}")
|
|
66
|
-
return sql.getvalue()
|
|
67
|
-
|
|
68
|
-
def __str__(self) -> str:
|
|
69
|
-
return self.sql
|
|
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
|