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.
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlqb
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A simple sql query builder
5
5
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlqb"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "A simple sql query builder"
5
5
  readme = "README.md"
6
6
  authors = []
@@ -0,0 +1,6 @@
1
+ from .select import Select
2
+ from .insert import Insert
3
+ from .delete import Delete
4
+ from .update import Update
5
+
6
+ __all__ = ["Select", "Insert", "Delete", "Update"]
@@ -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 = %s", 1)
48
- assert q.sql == "SELECT id FROM users WHERE id = %s"
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 = %s", True).Where("role = %s", "admin")
53
- assert q.sql == "SELECT id FROM users WHERE active = %s AND role = %s"
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 = %s AND b = %s", 1, 2)
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 = %s", "active")
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 = %s", "sale").Where("u.active = %s", True)
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 = %s", 42).Where("active = %s", True).Where("role IN (%s, %s)", "admin", "mod")
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 = %s", True).GroupBy("u.id", "u.name").OrderBy("u.name").Limit(25).Offset(50).sql
173
- assert sql == ("SELECT u.id, u.name FROM users u JOIN orders o ON o.user_id = u.id WHERE u.active = %s GROUP BY u.id, u.name ORDER BY u.name ASC LIMIT 25 OFFSET 50")
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]
@@ -188,7 +188,7 @@ wheels = [
188
188
 
189
189
  [[package]]
190
190
  name = "sqlqb"
191
- version = "0.1.0"
191
+ version = "0.1.2"
192
192
  source = { editable = "." }
193
193
 
194
194
  [package.dev-dependencies]
@@ -1,3 +0,0 @@
1
- from .select import Select
2
-
3
- __all__ = ["Select"]
File without changes
File without changes
File without changes
File without changes