sqlqb 0.1.3__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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python -m pytest tests/ -v)"
5
+ ]
6
+ }
7
+ }
sqlqb-0.2.0/.envrc ADDED
@@ -0,0 +1,3 @@
1
+ layout_ide
2
+
3
+ export PYTHONSTARTUP=.pythonrc.py
@@ -0,0 +1 @@
1
+ from sqlqb import * # noqa: F403
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlqb
3
- Version: 0.1.3
3
+ Version: 0.2.0
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.3"
3
+ version = "0.2.0"
4
4
  description = "A simple sql query builder"
5
5
  readme = "README.md"
6
6
  authors = []
@@ -5,40 +5,31 @@ class Delete:
5
5
  def __init__(self):
6
6
  self.__table: str | None = None
7
7
  self.__wheres: list[str] = []
8
- self.__params: list = []
9
- self.__limit: int | None = None
8
+ self.__params: dict = {}
10
9
 
11
10
  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
11
  self.__table = table
15
12
  return self
16
13
 
17
- def Where(self, condition: str, *args) -> "Delete":
14
+ def Where(self, condition: str, **kwargs) -> "Delete":
18
15
  self.__wheres.append(condition)
19
- self.__params += args
20
- return self
21
-
22
- def Limit(self, limit: int) -> "Delete":
23
- self.__limit = limit
16
+ self.__params.update(kwargs)
24
17
  return self
25
18
 
26
19
  @property
27
- def params(self) -> list:
20
+ def params(self) -> dict:
28
21
  return self.__params
29
22
 
30
23
  @property
31
24
  def sql(self) -> str:
32
- if not self.__table:
33
- raise ValueError("error: no table specified")
25
+ if not self.__table or not str(self.__table).strip():
26
+ raise ValueError("error: table name must not be empty")
34
27
  sql = io.StringIO()
35
28
  sql.write("DELETE FROM ")
36
29
  sql.write(self.__table)
37
30
  if self.__wheres:
38
31
  sql.write(" WHERE ")
39
32
  sql.write(" AND ".join(self.__wheres))
40
- if self.__limit is not None:
41
- sql.write(f" LIMIT {self.__limit}")
42
33
  return sql.getvalue()
43
34
 
44
35
  def __str__(self) -> str:
@@ -4,7 +4,7 @@ import io
4
4
  class Insert:
5
5
  def __init__(self):
6
6
  self.__table: str | None = None
7
- self.__rows: list[dict] = []
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.__rows.append(kwargs)
18
+ self.__values.append(kwargs)
19
19
  return self
20
20
 
21
21
  @property
22
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()]
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.__rows:
29
+ if not self.__values:
32
30
  raise ValueError("error: no values specified")
33
- columns = list(self.__rows[0].keys())
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
- placeholder = "(" + ", ".join(["?"] * len(columns)) + ")"
41
- sql.write(", ".join([placeholder] * len(self.__rows)))
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:
@@ -1,3 +1,5 @@
1
+ import re
2
+
1
3
  from pymysql.connections import Connection as _Connection
2
4
  from pymysql.cursors import DictCursor
3
5
 
@@ -7,25 +9,35 @@ from sqlqb import Update as _Update
7
9
  from sqlqb import Delete as _Delete
8
10
 
9
11
 
12
+ def _to_pymysql(sql: str, params: list | dict) -> tuple[str, list | dict]:
13
+ """Convert :name placeholders to %(name)s style for PyMySQL when params is a dict."""
14
+ if isinstance(params, dict):
15
+ sql = re.sub(r":([a-zA-Z_][a-zA-Z0-9_]*)", r"%(\1)s", sql)
16
+ return sql, params
17
+
18
+
10
19
  class Select(_Select):
11
20
  def __init__(self, connection: _Connection, *args):
12
21
  super().__init__(*args)
13
22
  self.__connection = connection
14
23
 
15
24
  def execute(self):
25
+ sql, params = _to_pymysql(self.sql, self.params)
16
26
  cursor = self.__connection.cursor()
17
- cursor.execute(self.sql, self.params)
27
+ cursor.execute(sql, params)
18
28
  return cursor
19
29
 
20
30
  def fetchone(self):
31
+ sql, params = _to_pymysql(self.sql, self.params)
21
32
  with self.__connection.cursor() as cursor:
22
- cursor.execute(self.sql, self.params)
33
+ cursor.execute(sql, params)
23
34
  result = cursor.fetchone()
24
35
  return result
25
36
 
26
37
  def fetchall(self):
38
+ sql, params = _to_pymysql(self.sql, self.params)
27
39
  with self.__connection.cursor() as cursor:
28
- cursor.execute(self.sql, self.params)
40
+ cursor.execute(sql, params)
29
41
  result = cursor.fetchall()
30
42
  return result
31
43
 
@@ -36,8 +48,9 @@ class Insert(_Insert):
36
48
  self.__connection = connection
37
49
 
38
50
  def execute(self) -> int:
51
+ sql, params = _to_pymysql(self.sql, self.params)
39
52
  with self.__connection.cursor() as cursor:
40
- cursor.execute(self.sql, self.params)
53
+ cursor.execute(sql, params)
41
54
  return cursor.rowcount
42
55
 
43
56
 
@@ -47,8 +60,9 @@ class Update(_Update):
47
60
  self.__connection = connection
48
61
 
49
62
  def execute(self) -> int:
63
+ sql, params = _to_pymysql(self.sql, self.params)
50
64
  with self.__connection.cursor() as cursor:
51
- cursor.execute(self.sql, self.params)
65
+ cursor.execute(sql, params)
52
66
  return cursor.rowcount
53
67
 
54
68
 
@@ -58,8 +72,9 @@ class Delete(_Delete):
58
72
  self.__connection = connection
59
73
 
60
74
  def execute(self) -> int:
75
+ sql, params = _to_pymysql(self.sql, self.params)
61
76
  with self.__connection.cursor() as cursor:
62
- cursor.execute(self.sql, self.params)
77
+ cursor.execute(sql, params)
63
78
  return cursor.rowcount
64
79
 
65
80
 
@@ -3,20 +3,20 @@ import io
3
3
 
4
4
  class Select:
5
5
  def __init__(self, *args: str):
6
- self.__columns: list[str] = list(args)
6
+ self.__columns: tuple[str] = args
7
7
  self.__table: str | None = None
8
- self.__wheres: list[str] = []
9
- self.__params: list = []
10
8
  self.__joins: list[str] = []
9
+ self.__wheres: list[str] = []
11
10
  self.__orderby: list[tuple[str, str]] = []
12
11
  self.__groupby: list[str] = []
13
12
  self.__limit: int | None = None
14
13
  self.__offset: int | None = None
14
+ self.__params: dict = {}
15
15
 
16
16
  def Columns(self, *columns: str) -> "Select":
17
17
  if not columns:
18
18
  raise ValueError("error: specify at least one column")
19
- self.__columns = list(columns)
19
+ self.__columns = columns
20
20
  return self
21
21
 
22
22
  def From(self, table: str) -> "Select":
@@ -25,14 +25,14 @@ class Select:
25
25
  self.__table = table
26
26
  return self
27
27
 
28
- def Join(self, join: str, *args) -> "Select":
28
+ def Join(self, join: str, **kwargs) -> "Select":
29
29
  self.__joins.append(join)
30
- self.__params += args
30
+ self.__params.update(kwargs)
31
31
  return self
32
32
 
33
- def Where(self, condition: str, *args) -> "Select":
33
+ def Where(self, condition: str, **kwargs) -> "Select":
34
34
  self.__wheres.append(condition)
35
- self.__params += args
35
+ self.__params.update(kwargs)
36
36
  return self
37
37
 
38
38
  def OrderBy(self, column: str, direction: str = "ASC") -> "Select":
@@ -54,11 +54,13 @@ class Select:
54
54
  return self
55
55
 
56
56
  @property
57
- def params(self) -> list:
57
+ def params(self) -> dict:
58
58
  return self.__params
59
59
 
60
60
  @property
61
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")
62
64
  sql = io.StringIO()
63
65
  sql.write("SELECT ")
64
66
  sql.write(", ".join(self.__columns or "*"))
@@ -9,32 +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) -> tuple[str, dict]:
13
- """Replace ? placeholders with :p0, :p1, ... for SQLAlchemy text()."""
14
- named: dict = {}
15
- for i, val in enumerate(params):
16
- key = f"p{i}"
17
- sql = sql.replace("?", f":{key}", 1)
18
- named[key] = val
19
- return sql, named
20
-
21
-
22
12
  class Select(_Select):
23
13
  def __init__(self, connection: _Connection, *args):
24
14
  super().__init__(*args)
25
15
  self.__connection = connection
26
16
 
27
17
  def execute(self):
28
- sql, params = _bind(self.sql, self.params)
29
- return self.__connection.execute(text(sql), params)
18
+ return self.__connection.execute(text(self.sql), self.params)
30
19
 
31
20
  def fetchone(self):
32
- sql, params = _bind(self.sql, self.params)
33
21
  row = self.execute().mappings().fetchone()
34
22
  return dict(row) if row is not None else None
35
23
 
36
24
  def fetchall(self) -> list:
37
- sql, params = _bind(self.sql, self.params)
38
25
  return [dict(row) for row in self.execute().mappings()]
39
26
 
40
27
 
@@ -44,8 +31,7 @@ class Insert(_Insert):
44
31
  self.__connection = connection
45
32
 
46
33
  def execute(self) -> int:
47
- sql, params = _bind(self.sql, self.params)
48
- return self.__connection.execute(text(sql), params).rowcount
34
+ return self.__connection.execute(text(self.sql), self.params).rowcount
49
35
 
50
36
 
51
37
  class Update(_Update):
@@ -54,8 +40,7 @@ class Update(_Update):
54
40
  self.__connection = connection
55
41
 
56
42
  def execute(self) -> int:
57
- sql, params = _bind(self.sql, self.params)
58
- return self.__connection.execute(text(sql), params).rowcount
43
+ return self.__connection.execute(text(self.sql), self.params).rowcount
59
44
 
60
45
 
61
46
  class Delete(_Delete):
@@ -64,8 +49,7 @@ class Delete(_Delete):
64
49
  self.__connection = connection
65
50
 
66
51
  def execute(self) -> int:
67
- sql, params = _bind(self.sql, self.params)
68
- return self.__connection.execute(text(sql), params).rowcount
52
+ return self.__connection.execute(text(self.sql), self.params).rowcount
69
53
 
70
54
 
71
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
- return self.__connection.execute(self.sql, self.params).rowcount
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):
@@ -8,8 +8,7 @@ class Update:
8
8
  self.__table = table
9
9
  self.__sets: dict[str, object] = {}
10
10
  self.__wheres: list[str] = []
11
- self.__where_params: list = []
12
- self.__limit: int | None = None
11
+ self.__params: dict[str, object] = {}
13
12
 
14
13
  def Set(self, **kwargs) -> "Update":
15
14
  if not kwargs:
@@ -17,20 +16,14 @@ class Update:
17
16
  self.__sets.update(kwargs)
18
17
  return self
19
18
 
20
- def Where(self, condition: str, *args) -> "Update":
19
+ def Where(self, condition: str, **kwargs) -> "Update":
21
20
  self.__wheres.append(condition)
22
- self.__where_params += args
23
- return self
24
-
25
- def Limit(self, limit: int) -> "Update":
26
- self.__limit = limit
21
+ self.__params.update(kwargs)
27
22
  return self
28
23
 
29
24
  @property
30
- def params(self) -> list:
31
- if not self.__sets:
32
- return []
33
- return list(self.__sets.values()) + self.__where_params
25
+ def params(self) -> dict:
26
+ return self.__sets | self.__params
34
27
 
35
28
  @property
36
29
  def sql(self) -> str:
@@ -40,12 +33,10 @@ class Update:
40
33
  sql.write("UPDATE ")
41
34
  sql.write(self.__table)
42
35
  sql.write(" SET ")
43
- sql.write(", ".join([f"{col} = ?" for col in self.__sets]))
36
+ sql.write(", ".join([f"{col} = :{col}" for col in self.__sets]))
44
37
  if self.__wheres:
45
38
  sql.write(" WHERE ")
46
39
  sql.write(" AND ".join(self.__wheres))
47
- if self.__limit is not None:
48
- sql.write(f" LIMIT {self.__limit}")
49
40
  return sql.getvalue()
50
41
 
51
42
  def __str__(self) -> str:
@@ -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 = ?", 1)
35
- assert q.sql == "DELETE FROM users WHERE id = ?"
36
- assert q.params == [1]
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 = ?", False).Where("role = ?", "guest")
40
- assert q.sql == "DELETE FROM users WHERE active = ? AND role = ?"
41
- assert q.params == [False, "guest"]
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 = ?", "debug").Limit(500)
80
- assert q.sql == "DELETE FROM logs WHERE level = ? LIMIT 500"
81
- assert q.params == ["debug"]
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 = ?", 1)
36
+ q = Select(conn, "name").From("users").Where("id = :id", id=1)
37
37
  q.execute()
38
- cursor.execute.assert_called_once_with(q.sql, q.params)
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 = ?", 1).fetchone()
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 = ?", 999).fetchone()
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 = ?", 1)
60
+ q = Select(conn, "name").From("users").Where("id = :id", id=1)
60
61
  q.fetchone()
61
- cursor.execute.assert_called_once_with(q.sql, q.params)
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 > ?", 18).Where("active = ?", True)
95
+ q = Select(conn, "name").From("users").Where("age > :age", age=18).Where("active = :active", active=True)
94
96
  q.fetchall()
95
- cursor.execute.assert_called_once_with(q.sql, [18, True])
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 >= ?", 18).OrderBy("name").Limit(1).fetchall()
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 = ?", 1).execute()
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 = ?", 1)
141
+ q = Update(conn, "users").Set(name="Alice").Where("id = :id", id=1)
139
142
  q.execute()
140
- cursor.execute.assert_called_once_with(q.sql, q.params)
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 = ?", 2)
160
+ q = Update(conn, "users").Set(name="Bob", age=20).Where("id = :id", id=2)
157
161
  count = q.execute()
158
- cursor.execute.assert_called_once_with(q.sql, q.params)
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 = ?", 1).execute()
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 = ?", 1)
176
+ q = Delete(conn).From("users").Where("id = :id", id=1)
172
177
  q.execute()
173
- cursor.execute.assert_called_once_with(q.sql, q.params)
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 = ?", 1).execute()
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
- def test_execute_with_limit(self, mock_conn):
187
- conn, cursor = mock_conn
188
- cursor.rowcount = 2
189
- q = Delete(conn).From("users").Limit(2)
190
- count = q.execute()
191
- cursor.execute.assert_called_once_with(q.sql, q.params)
192
- assert count == 2
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 = ?", 1)
62
- assert q.sql == "SELECT id FROM users WHERE id = ?"
63
- assert q.params == [1]
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 = ?", True).Where("role = ?", "admin")
67
- assert q.sql == "SELECT id FROM users WHERE active = ? AND role = ?"
68
- assert q.params == [True, "admin"]
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 = ?", "active")
96
- assert q.params == ["active"]
97
-
98
- def test_join_params_combined_with_where_params(self):
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)
100
- assert q.params == ["sale", True]
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 = 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")
185
- assert q.params == [42, True, "admin", "mod"]
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 = 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")
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 = ?", 1).fetchone()
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 = ?", 999).fetchone()
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 = ?", 999).fetchall()
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 >= ?", 18).fetchall()
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 >= ?", 18).Where("age <= ?", 29).fetchall()
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 = ?", 1).fetchone()
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 = ?", 4).fetchone()
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 = ?", 1).execute()
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 = ?", 1).execute()
187
- row = conn.Select("name").From("users").Where("id = ?", 1).fetchone()
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 = ?", 2).execute()
192
- row = conn.Select("name", "age").From("users").Where("id = ?", 2).fetchone()
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 = ?", 1).execute()
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 = ?", 4).execute()
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 = ?", 4).execute()
219
- row = conn.Select("*").From("users").Where("id = ?", 4).fetchone()
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):
@@ -1,13 +1,16 @@
1
1
  import pytest
2
2
 
3
- from sqlqb import sqlite
3
+ from sqlqb import sqlite3
4
4
 
5
5
 
6
6
  @pytest.fixture
7
7
  def conn():
8
- c = sqlite.connect(":memory:")
8
+ c = sqlite3.connect(":memory:")
9
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)])
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 = ?", 1).fetchone()
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 >= ?", 18).fetchall()
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 >= ?", 18).OrderBy("name").fetchall()
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).execute()
48
- row = conn.Select("name").From("users").Where("id = ?", 4).fetchone()
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 = ?", 4).fetchall()
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 = ?", 1).execute()
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 = ?", 1).execute()
68
- row = conn.Select("name").From("users").Where("id = ?", 1).fetchone()
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 = ?", 2).execute()
73
- row = conn.Select("name", "age").From("users").Where("id = ?", 2).fetchone()
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 = ?", 1).execute()
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 = ?", 1).execute()
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 = ?", 1).execute()
93
- row = conn.Select("*").From("users").Where("id = ?", 1).fetchone()
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
105
-
106
- with pytest.raises(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 == ["Alice"]
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 = ?, active = ?"
15
- assert q.params == ["Alice", True]
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 = ?" in q.sql
44
- assert "active = ?" in q.sql
45
- assert q.params == ["Alice", False]
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 == ["Bob"]
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 == [42, True, 3.14, None]
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 = ?", 1)
59
- assert q.sql == "UPDATE users SET active = ? WHERE id = ?"
60
- assert q.params == [False, 1]
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(role="guest").Where("active = ?", False).Where("role = ?", "admin")
64
- assert "WHERE active = ? AND role = ?" in q.sql
65
- assert q.params == ["guest", False, "admin"]
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 = ?", 2)
73
- assert q.params == [1, 2]
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 = ?", 42).Limit(1)
99
- assert q.sql == "UPDATE users SET role = ?, active = ? WHERE id = ? LIMIT 1"
100
- assert q.params == ["admin", True, 42]
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}
@@ -293,7 +293,7 @@ wheels = [
293
293
 
294
294
  [[package]]
295
295
  name = "sqlqb"
296
- version = "0.1.3"
296
+ version = "0.2.0"
297
297
  source = { editable = "." }
298
298
 
299
299
  [package.dev-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes