sqlqb 0.1.0__tar.gz → 0.1.2__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.0 → sqlqb-0.1.2}/PKG-INFO +1 -1
- {sqlqb-0.1.0 → sqlqb-0.1.2}/pyproject.toml +1 -1
- sqlqb-0.1.2/src/sqlqb/__init__.py +6 -0
- sqlqb-0.1.2/src/sqlqb/delete.py +45 -0
- sqlqb-0.1.2/src/sqlqb/insert.py +45 -0
- sqlqb-0.1.2/src/sqlqb/mysql/__init__.py +82 -0
- {sqlqb-0.1.0 → sqlqb-0.1.2}/src/sqlqb/select.py +19 -13
- sqlqb-0.1.2/src/sqlqb/sqlite/__init__.py +70 -0
- sqlqb-0.1.2/src/sqlqb/update.py +52 -0
- sqlqb-0.1.2/test.db +0 -0
- sqlqb-0.1.2/tests/test_delete.py +81 -0
- sqlqb-0.1.2/tests/test_insert.py +60 -0
- {sqlqb-0.1.0 → sqlqb-0.1.2}/tests/test_select.py +28 -10
- sqlqb-0.1.2/tests/test_sqlite.py +112 -0
- sqlqb-0.1.2/tests/test_update.py +100 -0
- {sqlqb-0.1.0 → sqlqb-0.1.2}/uv.lock +1 -1
- sqlqb-0.1.0/src/sqlqb/__init__.py +0 -3
- {sqlqb-0.1.0 → sqlqb-0.1.2}/.gitignore +0 -0
- {sqlqb-0.1.0 → sqlqb-0.1.2}/Makefile +0 -0
- {sqlqb-0.1.0 → sqlqb-0.1.2}/README.md +0 -0
- {sqlqb-0.1.0 → sqlqb-0.1.2}/tests/__init__.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
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.__limit: int | None = None
|
|
10
|
+
|
|
11
|
+
def From(self, table: str) -> "Delete":
|
|
12
|
+
if not table or not table.strip():
|
|
13
|
+
raise ValueError("error: table name must not be empty")
|
|
14
|
+
self.__table = table
|
|
15
|
+
return self
|
|
16
|
+
|
|
17
|
+
def Where(self, condition: str, *args) -> "Delete":
|
|
18
|
+
self.__wheres.append(condition)
|
|
19
|
+
self.__params += args
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def Limit(self, limit: int) -> "Delete":
|
|
23
|
+
self.__limit = limit
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def params(self) -> list:
|
|
28
|
+
return self.__params
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def sql(self) -> str:
|
|
32
|
+
if not self.__table:
|
|
33
|
+
raise ValueError("error: no table specified")
|
|
34
|
+
sql = io.StringIO()
|
|
35
|
+
sql.write("DELETE FROM ")
|
|
36
|
+
sql.write(self.__table)
|
|
37
|
+
if self.__wheres:
|
|
38
|
+
sql.write(" WHERE ")
|
|
39
|
+
sql.write(" AND ".join(self.__wheres))
|
|
40
|
+
if self.__limit is not None:
|
|
41
|
+
sql.write(f" LIMIT {self.__limit}")
|
|
42
|
+
return sql.getvalue()
|
|
43
|
+
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
return self.sql
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Insert:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.__table: str | None = None
|
|
7
|
+
self.__rows: list[dict] = []
|
|
8
|
+
|
|
9
|
+
def Into(self, table: str) -> "Insert":
|
|
10
|
+
if not table or not table.strip():
|
|
11
|
+
raise ValueError("error: table name must not be empty")
|
|
12
|
+
self.__table = table
|
|
13
|
+
return self
|
|
14
|
+
|
|
15
|
+
def Values(self, **kwargs) -> "Insert":
|
|
16
|
+
if not kwargs:
|
|
17
|
+
raise ValueError("error: specify at least one column value")
|
|
18
|
+
self.__rows.append(kwargs)
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def params(self) -> list:
|
|
23
|
+
if not self.__rows:
|
|
24
|
+
return []
|
|
25
|
+
return [v for row in self.__rows for v in row.values()]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def sql(self) -> str:
|
|
29
|
+
if not self.__table:
|
|
30
|
+
raise ValueError("error: no table specified")
|
|
31
|
+
if not self.__rows:
|
|
32
|
+
raise ValueError("error: no values specified")
|
|
33
|
+
columns = list(self.__rows[0].keys())
|
|
34
|
+
sql = io.StringIO()
|
|
35
|
+
sql.write("INSERT INTO ")
|
|
36
|
+
sql.write(self.__table)
|
|
37
|
+
sql.write(" (")
|
|
38
|
+
sql.write(", ".join(columns))
|
|
39
|
+
sql.write(") VALUES ")
|
|
40
|
+
placeholder = "(" + ", ".join(["?"] * len(columns)) + ")"
|
|
41
|
+
sql.write(", ".join([placeholder] * len(self.__rows)))
|
|
42
|
+
return sql.getvalue()
|
|
43
|
+
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
return self.sql
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from pymysql.connections import Connection as _Connection
|
|
2
|
+
from pymysql.cursors import DictCursor
|
|
3
|
+
|
|
4
|
+
from sqlqb import Select as _Select
|
|
5
|
+
from sqlqb import Insert as _Insert
|
|
6
|
+
from sqlqb import Update as _Update
|
|
7
|
+
from sqlqb import Delete as _Delete
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Select(_Select):
|
|
11
|
+
def __init__(self, connection, *args):
|
|
12
|
+
super().__init__(*args)
|
|
13
|
+
self.__connection = connection
|
|
14
|
+
|
|
15
|
+
def execute(self):
|
|
16
|
+
cursor = self.__connection.cursor()
|
|
17
|
+
cursor.execute(self.sql, self.params)
|
|
18
|
+
return cursor
|
|
19
|
+
|
|
20
|
+
def fetchone(self):
|
|
21
|
+
with self.__connection.cursor() as cursor:
|
|
22
|
+
cursor.execute(self.sql, self.params)
|
|
23
|
+
result = cursor.fetchone()
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
def fetchall(self):
|
|
27
|
+
with self.__connection.cursor() as cursor:
|
|
28
|
+
cursor.execute(self.sql, self.params)
|
|
29
|
+
result = cursor.fetchall()
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Insert(_Insert):
|
|
34
|
+
def __init__(self, connection):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.__connection = connection
|
|
37
|
+
|
|
38
|
+
def execute(self) -> int:
|
|
39
|
+
with self.__connection.cursor() as cursor:
|
|
40
|
+
cursor.execute(self.sql, self.params)
|
|
41
|
+
return cursor.rowcount
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Update(_Update):
|
|
45
|
+
def __init__(self, connection, table: str):
|
|
46
|
+
super().__init__(table)
|
|
47
|
+
self.__connection = connection
|
|
48
|
+
|
|
49
|
+
def execute(self) -> int:
|
|
50
|
+
with self.__connection.cursor() as cursor:
|
|
51
|
+
cursor.execute(self.sql, self.params)
|
|
52
|
+
return cursor.rowcount
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Delete(_Delete):
|
|
56
|
+
def __init__(self, connection):
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.__connection = connection
|
|
59
|
+
|
|
60
|
+
def execute(self) -> int:
|
|
61
|
+
with self.__connection.cursor() as cursor:
|
|
62
|
+
cursor.execute(self.sql, self.params)
|
|
63
|
+
return cursor.rowcount
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Connection(_Connection):
|
|
67
|
+
def __init__(self, *args, **kwargs):
|
|
68
|
+
if "cursorclass" not in kwargs:
|
|
69
|
+
kwargs["cursorclass"] = DictCursor
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
|
|
72
|
+
def Select(self, *args, **kwargs) -> Select:
|
|
73
|
+
return Select(self, *args, **kwargs)
|
|
74
|
+
|
|
75
|
+
def Insert(self) -> Insert:
|
|
76
|
+
return Insert(self)
|
|
77
|
+
|
|
78
|
+
def Delete(self) -> Delete:
|
|
79
|
+
return Delete(self)
|
|
80
|
+
|
|
81
|
+
def Update(self, table: str) -> Update:
|
|
82
|
+
return Update(self, table)
|
|
@@ -2,22 +2,26 @@ import io
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class Select:
|
|
5
|
-
def __init__(self, *args):
|
|
6
|
-
self.__columns = list(args)
|
|
7
|
-
self.__table = None
|
|
8
|
-
self.__wheres = []
|
|
9
|
-
self.__params = []
|
|
10
|
-
self.__joins = []
|
|
11
|
-
self.__orderby = []
|
|
12
|
-
self.__groupby = []
|
|
13
|
-
self.__limit = None
|
|
14
|
-
self.__offset = None
|
|
5
|
+
def __init__(self, *args: str):
|
|
6
|
+
self.__columns: list[str] = list(args)
|
|
7
|
+
self.__table: str | None = None
|
|
8
|
+
self.__wheres: list[str] = []
|
|
9
|
+
self.__params: list = []
|
|
10
|
+
self.__joins: list[str] = []
|
|
11
|
+
self.__orderby: list[tuple[str, str]] = []
|
|
12
|
+
self.__groupby: list[str] = []
|
|
13
|
+
self.__limit: int | None = None
|
|
14
|
+
self.__offset: int | None = None
|
|
15
15
|
|
|
16
|
-
def Columns(self, *columns) -> "Select":
|
|
16
|
+
def Columns(self, *columns: str) -> "Select":
|
|
17
|
+
if not columns:
|
|
18
|
+
raise ValueError("error: specify at least one column")
|
|
17
19
|
self.__columns = list(columns)
|
|
18
20
|
return self
|
|
19
21
|
|
|
20
22
|
def From(self, table: str) -> "Select":
|
|
23
|
+
if not table or not table.strip():
|
|
24
|
+
raise ValueError("error: table name must not be empty")
|
|
21
25
|
self.__table = table
|
|
22
26
|
return self
|
|
23
27
|
|
|
@@ -31,11 +35,13 @@ class Select:
|
|
|
31
35
|
self.__params += args
|
|
32
36
|
return self
|
|
33
37
|
|
|
34
|
-
def OrderBy(self, column: str, direction="ASC") -> "Select":
|
|
38
|
+
def OrderBy(self, column: str, direction: str = "ASC") -> "Select":
|
|
39
|
+
if direction not in ("ASC", "DESC"):
|
|
40
|
+
raise ValueError("error: invalid order by direction")
|
|
35
41
|
self.__orderby.append((column, direction))
|
|
36
42
|
return self
|
|
37
43
|
|
|
38
|
-
def GroupBy(self, *columns) -> "Select":
|
|
44
|
+
def GroupBy(self, *columns: str) -> "Select":
|
|
39
45
|
self.__groupby += columns
|
|
40
46
|
return self
|
|
41
47
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
|
|
3
|
+
from sqlqb import Select as _Select
|
|
4
|
+
from sqlqb import Insert as _Insert
|
|
5
|
+
from sqlqb import Update as _Update
|
|
6
|
+
from sqlqb import Delete as _Delete
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Select(_Select):
|
|
10
|
+
def __init__(self, connection, *args):
|
|
11
|
+
super().__init__(*args)
|
|
12
|
+
self.__connection = connection
|
|
13
|
+
|
|
14
|
+
def execute(self) -> sqlite3.Cursor:
|
|
15
|
+
return self.__connection.execute(self.sql, self.params)
|
|
16
|
+
|
|
17
|
+
def fetchone(self):
|
|
18
|
+
return self.execute().fetchone()
|
|
19
|
+
|
|
20
|
+
def fetchall(self) -> list:
|
|
21
|
+
return self.execute().fetchall()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Insert(_Insert):
|
|
25
|
+
def __init__(self, connection):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.__connection = connection
|
|
28
|
+
|
|
29
|
+
def execute(self) -> int:
|
|
30
|
+
return self.__connection.execute(self.sql, self.params).rowcount
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Update(_Update):
|
|
34
|
+
def __init__(self, connection, table: str):
|
|
35
|
+
super().__init__(table)
|
|
36
|
+
self.__connection = connection
|
|
37
|
+
|
|
38
|
+
def execute(self) -> int:
|
|
39
|
+
return self.__connection.execute(self.sql, self.params).rowcount
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Delete(_Delete):
|
|
43
|
+
def __init__(self, connection):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.__connection = connection
|
|
46
|
+
|
|
47
|
+
def execute(self) -> int:
|
|
48
|
+
return self.__connection.execute(self.sql, self.params).rowcount
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Connection(sqlite3.Connection):
|
|
52
|
+
def __init__(self, *args, **kwargs):
|
|
53
|
+
super().__init__(*args, **kwargs)
|
|
54
|
+
self.row_factory = lambda cursor, row: dict(zip([col[0] for col in cursor.description], row))
|
|
55
|
+
|
|
56
|
+
def Select(self, *args, **kwargs) -> Select:
|
|
57
|
+
return Select(self, *args, **kwargs)
|
|
58
|
+
|
|
59
|
+
def Insert(self) -> Insert:
|
|
60
|
+
return Insert(self)
|
|
61
|
+
|
|
62
|
+
def Delete(self) -> Delete:
|
|
63
|
+
return Delete(self)
|
|
64
|
+
|
|
65
|
+
def Update(self, table: str) -> Update:
|
|
66
|
+
return Update(self, table)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def connect(database, **kwargs) -> Connection:
|
|
70
|
+
return sqlite3.connect(database, factory=Connection, **kwargs)
|
|
@@ -0,0 +1,52 @@
|
|
|
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.__limit: int | None = None
|
|
13
|
+
|
|
14
|
+
def Set(self, **kwargs) -> "Update":
|
|
15
|
+
if not kwargs:
|
|
16
|
+
raise ValueError("error: specify at least one column value")
|
|
17
|
+
self.__sets.update(kwargs)
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
def Where(self, condition: str, *args) -> "Update":
|
|
21
|
+
self.__wheres.append(condition)
|
|
22
|
+
self.__where_params += args
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def Limit(self, limit: int) -> "Update":
|
|
26
|
+
self.__limit = limit
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def params(self) -> list:
|
|
31
|
+
if not self.__sets:
|
|
32
|
+
return []
|
|
33
|
+
return list(self.__sets.values()) + self.__where_params
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def sql(self) -> str:
|
|
37
|
+
if not self.__sets:
|
|
38
|
+
raise ValueError("error: no SET values specified")
|
|
39
|
+
sql = io.StringIO()
|
|
40
|
+
sql.write("UPDATE ")
|
|
41
|
+
sql.write(self.__table)
|
|
42
|
+
sql.write(" SET ")
|
|
43
|
+
sql.write(", ".join([f"{col} = ?" for col in self.__sets]))
|
|
44
|
+
if self.__wheres:
|
|
45
|
+
sql.write(" WHERE ")
|
|
46
|
+
sql.write(" AND ".join(self.__wheres))
|
|
47
|
+
if self.__limit is not None:
|
|
48
|
+
sql.write(f" LIMIT {self.__limit}")
|
|
49
|
+
return sql.getvalue()
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
return self.sql
|
sqlqb-0.1.2/test.db
ADDED
|
Binary file
|
|
@@ -0,0 +1,81 @@
|
|
|
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("")
|
|
22
|
+
|
|
23
|
+
def test_blank_table_raises(self):
|
|
24
|
+
with pytest.raises(ValueError):
|
|
25
|
+
Delete().From(" ")
|
|
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 = ?", 1)
|
|
35
|
+
assert q.sql == "DELETE FROM users WHERE id = ?"
|
|
36
|
+
assert q.params == [1]
|
|
37
|
+
|
|
38
|
+
def test_multiple_wheres_joined_with_and(self):
|
|
39
|
+
q = Delete().From("users").Where("active = ?", False).Where("role = ?", "guest")
|
|
40
|
+
assert q.sql == "DELETE FROM users WHERE active = ? AND role = ?"
|
|
41
|
+
assert q.params == [False, "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_where_multiple_params(self):
|
|
49
|
+
q = Delete().From("t").Where("a = ? AND b = ?", 1, 2)
|
|
50
|
+
assert q.params == [1, 2]
|
|
51
|
+
|
|
52
|
+
def test_no_where_absent(self):
|
|
53
|
+
q = Delete().From("users")
|
|
54
|
+
assert "WHERE" not in q.sql
|
|
55
|
+
|
|
56
|
+
|
|
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
|
+
class TestDeleteChaining:
|
|
72
|
+
def test_method_chaining_returns_delete(self):
|
|
73
|
+
q = Delete()
|
|
74
|
+
assert q.From("t") is q
|
|
75
|
+
assert q.Where("1=1") is q
|
|
76
|
+
assert q.Limit(5) is q
|
|
77
|
+
|
|
78
|
+
def test_full_query(self):
|
|
79
|
+
q = Delete().From("logs").Where("level = ?", "debug").Limit(500)
|
|
80
|
+
assert q.sql == "DELETE FROM logs WHERE level = ? LIMIT 500"
|
|
81
|
+
assert q.params == ["debug"]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from sqlqb import Insert
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestInsertBasic:
|
|
7
|
+
def test_single_row(self):
|
|
8
|
+
q = Insert().Into("users").Values(name="Alice", email="alice@example.com")
|
|
9
|
+
assert q.sql == "INSERT INTO users (name, email) VALUES (?, ?)"
|
|
10
|
+
|
|
11
|
+
def test_str_equals_sql(self):
|
|
12
|
+
q = Insert().Into("users").Values(name="Alice")
|
|
13
|
+
assert str(q) == q.sql
|
|
14
|
+
|
|
15
|
+
def test_params_single_row(self):
|
|
16
|
+
q = Insert().Into("users").Values(name="Alice", email="alice@example.com")
|
|
17
|
+
assert q.params == ["Alice", "alice@example.com"]
|
|
18
|
+
|
|
19
|
+
def test_multiple_rows(self):
|
|
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"]
|
|
23
|
+
|
|
24
|
+
def test_multiple_columns_multiple_rows(self):
|
|
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]
|
|
28
|
+
|
|
29
|
+
def test_method_chaining_returns_insert(self):
|
|
30
|
+
q = Insert()
|
|
31
|
+
assert q.Into("t") is q
|
|
32
|
+
assert q.Values(x=1) is q
|
|
33
|
+
|
|
34
|
+
def test_no_table_raises(self):
|
|
35
|
+
with pytest.raises(ValueError):
|
|
36
|
+
Insert().Values(name="Alice").sql
|
|
37
|
+
|
|
38
|
+
def test_no_values_raises(self):
|
|
39
|
+
with pytest.raises(ValueError):
|
|
40
|
+
Insert().Into("users").sql
|
|
41
|
+
|
|
42
|
+
def test_empty_table_raises(self):
|
|
43
|
+
with pytest.raises(ValueError):
|
|
44
|
+
Insert().Into("")
|
|
45
|
+
|
|
46
|
+
def test_blank_table_raises(self):
|
|
47
|
+
with pytest.raises(ValueError):
|
|
48
|
+
Insert().Into(" ")
|
|
49
|
+
|
|
50
|
+
def test_empty_values_raises(self):
|
|
51
|
+
with pytest.raises(ValueError):
|
|
52
|
+
Insert().Into("users").Values()
|
|
53
|
+
|
|
54
|
+
def test_params_empty_before_values(self):
|
|
55
|
+
q = Insert().Into("users")
|
|
56
|
+
assert q.params == []
|
|
57
|
+
|
|
58
|
+
def test_various_value_types(self):
|
|
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]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
1
3
|
from sqlqb import Select
|
|
2
4
|
|
|
3
5
|
|
|
@@ -30,6 +32,18 @@ class TestSelectBasic:
|
|
|
30
32
|
q = Select("id").From("orders")
|
|
31
33
|
assert "FROM orders" in q.sql
|
|
32
34
|
|
|
35
|
+
def test_columns_empty_raises(self):
|
|
36
|
+
with pytest.raises(ValueError):
|
|
37
|
+
Select().Columns()
|
|
38
|
+
|
|
39
|
+
def test_from_empty_raises(self):
|
|
40
|
+
with pytest.raises(ValueError):
|
|
41
|
+
Select().From("")
|
|
42
|
+
|
|
43
|
+
def test_from_blank_raises(self):
|
|
44
|
+
with pytest.raises(ValueError):
|
|
45
|
+
Select().From(" ")
|
|
46
|
+
|
|
33
47
|
def test_method_chaining_returns_select(self):
|
|
34
48
|
q = Select()
|
|
35
49
|
assert q.From("t") is q
|
|
@@ -44,13 +58,13 @@ class TestSelectBasic:
|
|
|
44
58
|
|
|
45
59
|
class TestWhere:
|
|
46
60
|
def test_single_where(self):
|
|
47
|
-
q = Select("id").From("users").Where("id =
|
|
48
|
-
assert q.sql == "SELECT id FROM users WHERE id =
|
|
61
|
+
q = Select("id").From("users").Where("id = ?", 1)
|
|
62
|
+
assert q.sql == "SELECT id FROM users WHERE id = ?"
|
|
49
63
|
assert q.params == [1]
|
|
50
64
|
|
|
51
65
|
def test_multiple_wheres_joined_with_and(self):
|
|
52
|
-
q = Select("id").From("users").Where("active =
|
|
53
|
-
assert q.sql == "SELECT id FROM users WHERE active =
|
|
66
|
+
q = Select("id").From("users").Where("active = ?", True).Where("role = ?", "admin")
|
|
67
|
+
assert q.sql == "SELECT id FROM users WHERE active = ? AND role = ?"
|
|
54
68
|
assert q.params == [True, "admin"]
|
|
55
69
|
|
|
56
70
|
def test_where_no_params(self):
|
|
@@ -59,7 +73,7 @@ class TestWhere:
|
|
|
59
73
|
assert q.params == []
|
|
60
74
|
|
|
61
75
|
def test_where_multiple_params(self):
|
|
62
|
-
q = Select().From("t").Where("a =
|
|
76
|
+
q = Select().From("t").Where("a = ? AND b = ?", 1, 2)
|
|
63
77
|
assert q.params == [1, 2]
|
|
64
78
|
|
|
65
79
|
def test_no_where_clause_absent(self):
|
|
@@ -78,11 +92,11 @@ class TestJoin:
|
|
|
78
92
|
assert "JOIN items i ON i.order_id = o.id" in q.sql
|
|
79
93
|
|
|
80
94
|
def test_join_with_params(self):
|
|
81
|
-
q = Select("u.id").From("users u").Join("JOIN orders o ON o.user_id = u.id AND o.status =
|
|
95
|
+
q = Select("u.id").From("users u").Join("JOIN orders o ON o.user_id = u.id AND o.status = ?", "active")
|
|
82
96
|
assert q.params == ["active"]
|
|
83
97
|
|
|
84
98
|
def test_join_params_combined_with_where_params(self):
|
|
85
|
-
q = Select("u.id").From("users u").Join("JOIN orders o ON o.user_id = u.id AND o.type =
|
|
99
|
+
q = Select("u.id").From("users u").Join("JOIN orders o ON o.user_id = u.id AND o.type = ?", "sale").Where("u.active = ?", True)
|
|
86
100
|
assert q.params == ["sale", True]
|
|
87
101
|
|
|
88
102
|
def test_no_join_absent(self):
|
|
@@ -103,6 +117,10 @@ class TestOrderBy:
|
|
|
103
117
|
q = Select("id").From("users").OrderBy("name").OrderBy("created_at", "DESC")
|
|
104
118
|
assert q.sql == "SELECT id FROM users ORDER BY name ASC, created_at DESC"
|
|
105
119
|
|
|
120
|
+
def test_invalid_direction_raises(self):
|
|
121
|
+
with pytest.raises(ValueError):
|
|
122
|
+
Select("id").From("users").OrderBy("id", "RANDOM")
|
|
123
|
+
|
|
106
124
|
def test_no_order_by_absent(self):
|
|
107
125
|
q = Select("id").From("users")
|
|
108
126
|
assert "ORDER BY" not in q.sql
|
|
@@ -163,14 +181,14 @@ class TestParams:
|
|
|
163
181
|
assert q.params == []
|
|
164
182
|
|
|
165
183
|
def test_params_accumulate_across_joins_and_wheres(self):
|
|
166
|
-
q = Select("id").From("users").Join("JOIN t ON t.id = users.t_id AND t.x =
|
|
184
|
+
q = Select("id").From("users").Join("JOIN t ON t.id = users.t_id AND t.x = ?", 42).Where("active = ?", True).Where("role IN (?, ?)", "admin", "mod")
|
|
167
185
|
assert q.params == [42, True, "admin", "mod"]
|
|
168
186
|
|
|
169
187
|
|
|
170
188
|
class TestClauseOrdering:
|
|
171
189
|
def test_full_query_clause_order(self):
|
|
172
|
-
sql = Select("u.id", "u.name").From("users u").Join("JOIN orders o ON o.user_id = u.id").Where("u.active =
|
|
173
|
-
assert sql == ("SELECT u.id, u.name FROM users u JOIN orders o ON o.user_id = u.id WHERE u.active =
|
|
190
|
+
sql = Select("u.id", "u.name").From("users u").Join("JOIN orders o ON o.user_id = u.id").Where("u.active = ?", True).GroupBy("u.id", "u.name").OrderBy("u.name").Limit(25).Offset(50).sql
|
|
191
|
+
assert sql == ("SELECT u.id, u.name FROM users u JOIN orders o ON o.user_id = u.id WHERE u.active = ? GROUP BY u.id, u.name ORDER BY u.name ASC LIMIT 25 OFFSET 50")
|
|
174
192
|
|
|
175
193
|
def test_group_by_before_order_by(self):
|
|
176
194
|
sql = Select("a").From("t").OrderBy("a").GroupBy("a").sql
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from sqlqb import sqlite
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def conn():
|
|
8
|
+
c = sqlite.connect(":memory:")
|
|
9
|
+
c.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
|
|
10
|
+
c.executemany("INSERT INTO users VALUES (?, ?, ?)", [(1, "Alice", 30), (2, "Bob", 17), (3, "Carol", 25)])
|
|
11
|
+
c.commit()
|
|
12
|
+
return c
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestSelect:
|
|
16
|
+
def test_fetchall(self, conn):
|
|
17
|
+
rows = conn.Select("id", "name").From("users").fetchall()
|
|
18
|
+
assert rows == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Carol"}]
|
|
19
|
+
|
|
20
|
+
def test_fetchone(self, conn):
|
|
21
|
+
row = conn.Select("name").From("users").Where("id = ?", 1).fetchone()
|
|
22
|
+
assert row == {"name": "Alice"}
|
|
23
|
+
|
|
24
|
+
def test_where_with_param(self, conn):
|
|
25
|
+
rows = conn.Select("name").From("users").Where("age >= ?", 18).fetchall()
|
|
26
|
+
assert rows == [{"name": "Alice"}, {"name": "Carol"}]
|
|
27
|
+
|
|
28
|
+
def test_execute_returns_cursor(self, conn):
|
|
29
|
+
cursor = conn.Select("*").From("users").execute()
|
|
30
|
+
assert cursor.fetchone() == {"id": 1, "name": "Alice", "age": 30}
|
|
31
|
+
|
|
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
|
+
)
|
|
40
|
+
assert rows == [{"name": "Alice"}, {"name": "Carol"}]
|
|
41
|
+
|
|
42
|
+
def test_limit(self, conn):
|
|
43
|
+
rows = conn.Select("id").From("users").Limit(2).fetchall()
|
|
44
|
+
assert rows == [{"id": 1}, {"id": 2}]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestInsert:
|
|
48
|
+
def test_insert_returns_rowcount(self, conn):
|
|
49
|
+
count = conn.Insert().Into("users").Values(id=4, name="Dave", age=40).execute()
|
|
50
|
+
assert count == 1
|
|
51
|
+
|
|
52
|
+
def test_insert_row_is_queryable(self, conn):
|
|
53
|
+
conn.Insert().Into("users").Values(id=4, name="Dave", age=40).execute()
|
|
54
|
+
row = conn.Select("name").From("users").Where("id = ?", 4).fetchone()
|
|
55
|
+
assert row == {"name": "Dave"}
|
|
56
|
+
|
|
57
|
+
def test_insert_multiple_rows(self, conn):
|
|
58
|
+
count = conn.Insert().Into("users").Values(id=4, name="Dave", age=40).Values(id=5, name="Eve", age=22).execute()
|
|
59
|
+
assert count == 2
|
|
60
|
+
|
|
61
|
+
def test_insert_multiple_rows_are_queryable(self, conn):
|
|
62
|
+
conn.Insert().Into("users").Values(id=4, name="Dave", age=40).Values(id=5, name="Eve", age=22).execute()
|
|
63
|
+
rows = conn.Select("name").From("users").Where("id = ?", 4).fetchall()
|
|
64
|
+
assert rows == [{"name": "Dave"}]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestUpdate:
|
|
68
|
+
def test_update_returns_rowcount(self, conn):
|
|
69
|
+
count = conn.Update("users").Set(name="Alicia").Where("id = ?", 1).execute()
|
|
70
|
+
assert count == 1
|
|
71
|
+
|
|
72
|
+
def test_update_is_reflected(self, conn):
|
|
73
|
+
conn.Update("users").Set(name="Alicia").Where("id = ?", 1).execute()
|
|
74
|
+
row = conn.Select("name").From("users").Where("id = ?", 1).fetchone()
|
|
75
|
+
assert row == {"name": "Alicia"}
|
|
76
|
+
|
|
77
|
+
def test_update_multiple_columns(self, conn):
|
|
78
|
+
conn.Update("users").Set(name="Bobby", age=18).Where("id = ?", 2).execute()
|
|
79
|
+
row = conn.Select("name", "age").From("users").Where("id = ?", 2).fetchone()
|
|
80
|
+
assert row == {"name": "Bobby", "age": 18}
|
|
81
|
+
|
|
82
|
+
def test_update_affects_correct_rows_only(self, conn):
|
|
83
|
+
conn.Update("users").Set(age=99).Where("id = ?", 1).execute()
|
|
84
|
+
rows = conn.Select("age").From("users").fetchall()
|
|
85
|
+
assert rows == [{"age": 99}, {"age": 17}, {"age": 25}]
|
|
86
|
+
|
|
87
|
+
def test_update_no_where_affects_all(self, conn):
|
|
88
|
+
count = conn.Update("users").Set(age=0).execute()
|
|
89
|
+
assert count == 3
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestDelete:
|
|
93
|
+
def test_delete_returns_rowcount(self, conn):
|
|
94
|
+
count = conn.Delete().From("users").Where("id = ?", 1).execute()
|
|
95
|
+
assert count == 1
|
|
96
|
+
|
|
97
|
+
def test_delete_row_is_gone(self, conn):
|
|
98
|
+
conn.Delete().From("users").Where("id = ?", 1).execute()
|
|
99
|
+
row = conn.Select("*").From("users").Where("id = ?", 1).fetchone()
|
|
100
|
+
assert row is None
|
|
101
|
+
|
|
102
|
+
def test_delete_no_where_clears_table(self, conn):
|
|
103
|
+
count = conn.Delete().From("users").execute()
|
|
104
|
+
assert count == 3
|
|
105
|
+
rows = conn.Select("*").From("users").fetchall()
|
|
106
|
+
assert rows == []
|
|
107
|
+
|
|
108
|
+
def test_delete_with_limit_not_supported(self, conn):
|
|
109
|
+
# NOTE: sqlite library of python does not support LIMIT in delete queries
|
|
110
|
+
import sqlite3
|
|
111
|
+
with pytest.raises(sqlite3.OperationalError):
|
|
112
|
+
conn.Delete().From("users").Limit(2).execute()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from sqlqb import Update
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestUpdateBasic:
|
|
7
|
+
def test_single_column(self):
|
|
8
|
+
q = Update("users").Set(name="Alice")
|
|
9
|
+
assert q.sql == "UPDATE users SET name = ?"
|
|
10
|
+
assert q.params == ["Alice"]
|
|
11
|
+
|
|
12
|
+
def test_multiple_columns(self):
|
|
13
|
+
q = Update("users").Set(name="Alice", active=True)
|
|
14
|
+
assert q.sql == "UPDATE users SET name = ?, active = ?"
|
|
15
|
+
assert q.params == ["Alice", True]
|
|
16
|
+
|
|
17
|
+
def test_str_equals_sql(self):
|
|
18
|
+
q = Update("users").Set(name="Alice")
|
|
19
|
+
assert str(q) == q.sql
|
|
20
|
+
|
|
21
|
+
def test_empty_table_raises(self):
|
|
22
|
+
with pytest.raises(ValueError):
|
|
23
|
+
Update("")
|
|
24
|
+
|
|
25
|
+
def test_blank_table_raises(self):
|
|
26
|
+
with pytest.raises(ValueError):
|
|
27
|
+
Update(" ")
|
|
28
|
+
|
|
29
|
+
def test_no_set_sql_raises(self):
|
|
30
|
+
with pytest.raises(ValueError):
|
|
31
|
+
Update("users").sql
|
|
32
|
+
|
|
33
|
+
def test_empty_set_raises(self):
|
|
34
|
+
with pytest.raises(ValueError):
|
|
35
|
+
Update("users").Set()
|
|
36
|
+
|
|
37
|
+
def test_params_empty_before_set(self):
|
|
38
|
+
q = Update("users")
|
|
39
|
+
assert q.params == []
|
|
40
|
+
|
|
41
|
+
def test_chained_set_merges_columns(self):
|
|
42
|
+
q = Update("users").Set(name="Alice").Set(active=False)
|
|
43
|
+
assert "name = ?" in q.sql
|
|
44
|
+
assert "active = ?" in q.sql
|
|
45
|
+
assert q.params == ["Alice", False]
|
|
46
|
+
|
|
47
|
+
def test_chained_set_overwrites_same_column(self):
|
|
48
|
+
q = Update("users").Set(name="Alice").Set(name="Bob")
|
|
49
|
+
assert q.params == ["Bob"]
|
|
50
|
+
|
|
51
|
+
def test_various_value_types(self):
|
|
52
|
+
q = Update("t").Set(x=42, flag=True, score=3.14, label=None)
|
|
53
|
+
assert q.params == [42, True, 3.14, None]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestUpdateWhere:
|
|
57
|
+
def test_single_where(self):
|
|
58
|
+
q = Update("users").Set(active=False).Where("id = ?", 1)
|
|
59
|
+
assert q.sql == "UPDATE users SET active = ? WHERE id = ?"
|
|
60
|
+
assert q.params == [False, 1]
|
|
61
|
+
|
|
62
|
+
def test_multiple_wheres_joined_with_and(self):
|
|
63
|
+
q = Update("users").Set(role="guest").Where("active = ?", False).Where("role = ?", "admin")
|
|
64
|
+
assert "WHERE active = ? AND role = ?" in q.sql
|
|
65
|
+
assert q.params == ["guest", False, "admin"]
|
|
66
|
+
|
|
67
|
+
def test_where_no_params(self):
|
|
68
|
+
q = Update("sessions").Set(expired=True).Where("token IS NOT NULL")
|
|
69
|
+
assert "WHERE token IS NOT NULL" in q.sql
|
|
70
|
+
|
|
71
|
+
def test_set_params_before_where_params(self):
|
|
72
|
+
q = Update("t").Set(x=1).Where("y = ?", 2)
|
|
73
|
+
assert q.params == [1, 2]
|
|
74
|
+
|
|
75
|
+
def test_no_where_absent(self):
|
|
76
|
+
q = Update("users").Set(active=True)
|
|
77
|
+
assert "WHERE" not in q.sql
|
|
78
|
+
|
|
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
|
+
class TestUpdateChaining:
|
|
91
|
+
def test_method_chaining_returns_update(self):
|
|
92
|
+
q = Update("t")
|
|
93
|
+
assert q.Set(x=1) is q
|
|
94
|
+
assert q.Where("1=1") is q
|
|
95
|
+
assert q.Limit(5) is q
|
|
96
|
+
|
|
97
|
+
def test_full_query(self):
|
|
98
|
+
q = Update("users").Set(role="admin", active=True).Where("id = ?", 42).Limit(1)
|
|
99
|
+
assert q.sql == "UPDATE users SET role = ?, active = ? WHERE id = ? LIMIT 1"
|
|
100
|
+
assert q.params == ["admin", True, 42]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|