t-sql 2.2.0__tar.gz → 3.0.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.
- {t_sql-2.2.0 → t_sql-3.0.0}/PKG-INFO +97 -1
- {t_sql-2.2.0 → t_sql-3.0.0}/README.md +96 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/pyproject.toml +1 -1
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_helper_functions.py +2 -7
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_mysql_integration.py +2 -2
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_query_builder.py +9 -9
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_sqlite_integration.py +3 -3
- {t_sql-2.2.0 → t_sql-3.0.0}/tsql/__init__.py +8 -4
- {t_sql-2.2.0 → t_sql-3.0.0}/tsql/query_builder.py +6 -2
- t_sql-2.2.0/RELEASE_NOTES.md +0 -41
- {t_sql-2.2.0 → t_sql-3.0.0}/.dockerignore +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/.github/workflows/publish.yml +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/.github/workflows/test.yml +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/.gitignore +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/Dockerfile +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/LICENSE +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/compose.yaml +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/context7.json +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/pytest.ini +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_alembic_integration.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_different_object_types.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_escaped.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_parameter_names.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_styles.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tests/test_tsql.py +0 -0
- {t_sql-2.2.0 → t_sql-3.0.0}/tsql/styles.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: t-sql
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
|
|
5
5
|
Project-URL: Homepage, https://github.com/nhumrich/t-sql
|
|
6
6
|
License-File: LICENSE
|
|
@@ -559,3 +559,99 @@ execute_sql_query("SELECT * FROM users") # ✗ Type error!
|
|
|
559
559
|
```
|
|
560
560
|
|
|
561
561
|
The `TSQLQuery` type is a union of `TSQL`, `Template` (t-strings), and `QueryBuilder`, ensuring all queries are safe from SQL injection.
|
|
562
|
+
|
|
563
|
+
# Security Considerations
|
|
564
|
+
|
|
565
|
+
## Overview
|
|
566
|
+
|
|
567
|
+
SQL injection is one of the most critical web application security risks (OWASP Top 10). This library is designed from the ground up to prevent SQL injection attacks through multiple layers of protection. However, understanding how these protections work—and where they can be bypassed—is essential for secure usage.
|
|
568
|
+
|
|
569
|
+
## How t-sql Prevents SQL Injection
|
|
570
|
+
|
|
571
|
+
### 1. Automatic Parameterization (Primary Defense)
|
|
572
|
+
|
|
573
|
+
By default, all interpolated values in t-strings are converted to parameterized queries:
|
|
574
|
+
|
|
575
|
+
```python
|
|
576
|
+
# User input (potentially malicious)
|
|
577
|
+
user_input = "admin' OR 1=1 --"
|
|
578
|
+
|
|
579
|
+
# t-sql automatically parameterizes this
|
|
580
|
+
sql, params = tsql.render(t"SELECT * FROM users WHERE name = {user_input}")
|
|
581
|
+
# Result: ('SELECT * FROM users WHERE name = ?', ["admin' OR 1=1 --"])
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The malicious SQL becomes **literal string data** in the parameter, not executable SQL code. The database treats it as a string value to match, not as SQL syntax.
|
|
585
|
+
|
|
586
|
+
**Attack vectors prevented:**
|
|
587
|
+
- Classic injection: `' OR 1=1 --`
|
|
588
|
+
- Union-based: `' UNION SELECT * FROM secrets --`
|
|
589
|
+
- Stacked queries: `'; DROP TABLE users; --`
|
|
590
|
+
- Boolean-based blind: `' AND SLEEP(5) --`
|
|
591
|
+
- Authentication bypass: `admin'--`
|
|
592
|
+
|
|
593
|
+
### 2. Literal Validation (Identifier Safety)
|
|
594
|
+
|
|
595
|
+
For table and column names that cannot be parameterized, use `:literal`:
|
|
596
|
+
|
|
597
|
+
```python
|
|
598
|
+
table = "users"
|
|
599
|
+
col = "name"
|
|
600
|
+
sql, params = tsql.render(t"SELECT * FROM {table:literal} WHERE {col:literal} = {value}")
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Validation rules:**
|
|
604
|
+
- Must be valid Python identifiers (`str.isidentifier()`)
|
|
605
|
+
- Supports qualified names: `table.column` or `schema.table.column` (max 3 parts)
|
|
606
|
+
- Rejects anything with spaces, quotes, or special characters
|
|
607
|
+
|
|
608
|
+
```python
|
|
609
|
+
# These are REJECTED with ValueError:
|
|
610
|
+
bad_table = "users; DROP TABLE secrets" # Contains semicolon
|
|
611
|
+
bad_col = "name' OR 1=1" # Contains quote
|
|
612
|
+
bad_schema = "schema.table.column.extra" # Too many parts
|
|
613
|
+
|
|
614
|
+
tsql.render(t"SELECT * FROM {bad_table:literal}") # Raises ValueError
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**Attack vectors prevented:**
|
|
618
|
+
- Table/column injection: `users; DROP TABLE secrets`
|
|
619
|
+
- Second-order injection via identifiers
|
|
620
|
+
- Schema manipulation
|
|
621
|
+
|
|
622
|
+
### 3. Escape-based Protection (ESCAPED Style)
|
|
623
|
+
|
|
624
|
+
For databases or scenarios where parameterization isn't available, the `ESCAPED` style properly escapes values:
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
malicious = "'; DROP TABLE users; --"
|
|
628
|
+
sql, _ = tsql.render(t"SELECT * FROM users WHERE name = {malicious}", style=tsql.styles.ESCAPED)
|
|
629
|
+
# Result: "SELECT * FROM users WHERE name = '''; DROP TABLE users; --'"
|
|
630
|
+
# (single quotes are doubled, making it literal data)
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**Important:** While effective, parameterization is always preferred when available. Use `ESCAPED` only when necessary.
|
|
634
|
+
|
|
635
|
+
## Danger Zones: Where You Can Still Get Hurt
|
|
636
|
+
|
|
637
|
+
### The :unsafe Format Spec
|
|
638
|
+
|
|
639
|
+
The `:unsafe` format spec **bypasses all safety mechanisms**:
|
|
640
|
+
|
|
641
|
+
```python
|
|
642
|
+
# DANGEROUS - no validation or parameterization!
|
|
643
|
+
dynamic_sql = "age > 18 OR role = 'admin'" # If this comes from user input, you're vulnerable
|
|
644
|
+
sql, params = tsql.render(t"SELECT * FROM users WHERE {dynamic_sql:unsafe}")
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**When :unsafe is acceptable:**
|
|
648
|
+
- Hard-coded SQL fragments in your own code
|
|
649
|
+
- SQL generated by trusted, validated builder logic
|
|
650
|
+
- Dynamic ORDER BY clauses (after validation)
|
|
651
|
+
|
|
652
|
+
**When :unsafe is DANGEROUS:**
|
|
653
|
+
- **Never** with user input (even "validated" input)
|
|
654
|
+
- Dynamic WHERE clauses from external sources
|
|
655
|
+
- Any data from forms, APIs, or databases
|
|
656
|
+
|
|
657
|
+
**Recommendation:** Treat `:unsafe` like `eval()` in your code reviews. Every usage should be scrutinized and documented.
|
|
@@ -549,3 +549,99 @@ execute_sql_query("SELECT * FROM users") # ✗ Type error!
|
|
|
549
549
|
```
|
|
550
550
|
|
|
551
551
|
The `TSQLQuery` type is a union of `TSQL`, `Template` (t-strings), and `QueryBuilder`, ensuring all queries are safe from SQL injection.
|
|
552
|
+
|
|
553
|
+
# Security Considerations
|
|
554
|
+
|
|
555
|
+
## Overview
|
|
556
|
+
|
|
557
|
+
SQL injection is one of the most critical web application security risks (OWASP Top 10). This library is designed from the ground up to prevent SQL injection attacks through multiple layers of protection. However, understanding how these protections work—and where they can be bypassed—is essential for secure usage.
|
|
558
|
+
|
|
559
|
+
## How t-sql Prevents SQL Injection
|
|
560
|
+
|
|
561
|
+
### 1. Automatic Parameterization (Primary Defense)
|
|
562
|
+
|
|
563
|
+
By default, all interpolated values in t-strings are converted to parameterized queries:
|
|
564
|
+
|
|
565
|
+
```python
|
|
566
|
+
# User input (potentially malicious)
|
|
567
|
+
user_input = "admin' OR 1=1 --"
|
|
568
|
+
|
|
569
|
+
# t-sql automatically parameterizes this
|
|
570
|
+
sql, params = tsql.render(t"SELECT * FROM users WHERE name = {user_input}")
|
|
571
|
+
# Result: ('SELECT * FROM users WHERE name = ?', ["admin' OR 1=1 --"])
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The malicious SQL becomes **literal string data** in the parameter, not executable SQL code. The database treats it as a string value to match, not as SQL syntax.
|
|
575
|
+
|
|
576
|
+
**Attack vectors prevented:**
|
|
577
|
+
- Classic injection: `' OR 1=1 --`
|
|
578
|
+
- Union-based: `' UNION SELECT * FROM secrets --`
|
|
579
|
+
- Stacked queries: `'; DROP TABLE users; --`
|
|
580
|
+
- Boolean-based blind: `' AND SLEEP(5) --`
|
|
581
|
+
- Authentication bypass: `admin'--`
|
|
582
|
+
|
|
583
|
+
### 2. Literal Validation (Identifier Safety)
|
|
584
|
+
|
|
585
|
+
For table and column names that cannot be parameterized, use `:literal`:
|
|
586
|
+
|
|
587
|
+
```python
|
|
588
|
+
table = "users"
|
|
589
|
+
col = "name"
|
|
590
|
+
sql, params = tsql.render(t"SELECT * FROM {table:literal} WHERE {col:literal} = {value}")
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
**Validation rules:**
|
|
594
|
+
- Must be valid Python identifiers (`str.isidentifier()`)
|
|
595
|
+
- Supports qualified names: `table.column` or `schema.table.column` (max 3 parts)
|
|
596
|
+
- Rejects anything with spaces, quotes, or special characters
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
# These are REJECTED with ValueError:
|
|
600
|
+
bad_table = "users; DROP TABLE secrets" # Contains semicolon
|
|
601
|
+
bad_col = "name' OR 1=1" # Contains quote
|
|
602
|
+
bad_schema = "schema.table.column.extra" # Too many parts
|
|
603
|
+
|
|
604
|
+
tsql.render(t"SELECT * FROM {bad_table:literal}") # Raises ValueError
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Attack vectors prevented:**
|
|
608
|
+
- Table/column injection: `users; DROP TABLE secrets`
|
|
609
|
+
- Second-order injection via identifiers
|
|
610
|
+
- Schema manipulation
|
|
611
|
+
|
|
612
|
+
### 3. Escape-based Protection (ESCAPED Style)
|
|
613
|
+
|
|
614
|
+
For databases or scenarios where parameterization isn't available, the `ESCAPED` style properly escapes values:
|
|
615
|
+
|
|
616
|
+
```python
|
|
617
|
+
malicious = "'; DROP TABLE users; --"
|
|
618
|
+
sql, _ = tsql.render(t"SELECT * FROM users WHERE name = {malicious}", style=tsql.styles.ESCAPED)
|
|
619
|
+
# Result: "SELECT * FROM users WHERE name = '''; DROP TABLE users; --'"
|
|
620
|
+
# (single quotes are doubled, making it literal data)
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Important:** While effective, parameterization is always preferred when available. Use `ESCAPED` only when necessary.
|
|
624
|
+
|
|
625
|
+
## Danger Zones: Where You Can Still Get Hurt
|
|
626
|
+
|
|
627
|
+
### The :unsafe Format Spec
|
|
628
|
+
|
|
629
|
+
The `:unsafe` format spec **bypasses all safety mechanisms**:
|
|
630
|
+
|
|
631
|
+
```python
|
|
632
|
+
# DANGEROUS - no validation or parameterization!
|
|
633
|
+
dynamic_sql = "age > 18 OR role = 'admin'" # If this comes from user input, you're vulnerable
|
|
634
|
+
sql, params = tsql.render(t"SELECT * FROM users WHERE {dynamic_sql:unsafe}")
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**When :unsafe is acceptable:**
|
|
638
|
+
- Hard-coded SQL fragments in your own code
|
|
639
|
+
- SQL generated by trusted, validated builder logic
|
|
640
|
+
- Dynamic ORDER BY clauses (after validation)
|
|
641
|
+
|
|
642
|
+
**When :unsafe is DANGEROUS:**
|
|
643
|
+
- **Never** with user input (even "validated" input)
|
|
644
|
+
- Dynamic WHERE clauses from external sources
|
|
645
|
+
- Any data from forms, APIs, or databases
|
|
646
|
+
|
|
647
|
+
**Recommendation:** Treat `:unsafe` like `eval()` in your code reviews. Every usage should be scrutinized and documented.
|
|
@@ -59,14 +59,9 @@ def test_insert():
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def test_update():
|
|
62
|
-
|
|
63
|
-
'name': 'Bob Updated',
|
|
64
|
-
'age': 35
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
query = tsql.update('users', values, 123)
|
|
62
|
+
query = tsql.update('users', 123, name='Bob Updated', age=35)
|
|
68
63
|
result = tsql.render(query)
|
|
69
|
-
|
|
64
|
+
|
|
70
65
|
assert "UPDATE users SET" in result[0]
|
|
71
66
|
assert "name = ?" in result[0]
|
|
72
67
|
assert "age = ?" in result[0]
|
|
@@ -170,7 +170,7 @@ async def test_update_without_returning(conn):
|
|
|
170
170
|
age: int
|
|
171
171
|
|
|
172
172
|
# Update without RETURNING
|
|
173
|
-
query = TestUsers.update(
|
|
173
|
+
query = TestUsers.update(age=31).where(TestUsers.name == 'Alice')
|
|
174
174
|
sql, params = query.render(style=tsql.styles.FORMAT)
|
|
175
175
|
|
|
176
176
|
# Should NOT have RETURNING clause
|
|
@@ -262,7 +262,7 @@ async def test_helper_functions(conn):
|
|
|
262
262
|
assert row[1] == 25
|
|
263
263
|
|
|
264
264
|
# Test update
|
|
265
|
-
query = tsql.update('test_users',
|
|
265
|
+
query = tsql.update('test_users', 1, age=26)
|
|
266
266
|
sql, params = query.render(style=tsql.styles.FORMAT)
|
|
267
267
|
|
|
268
268
|
await cursor.execute(sql, params)
|
|
@@ -351,7 +351,7 @@ def test_schema_in_update():
|
|
|
351
351
|
id: Column
|
|
352
352
|
name: Column
|
|
353
353
|
|
|
354
|
-
query = Accounts.update(
|
|
354
|
+
query = Accounts.update(name='Updated Account').where(Accounts.id == '1')
|
|
355
355
|
sql, params = query.render()
|
|
356
356
|
|
|
357
357
|
assert 'UPDATE other.accounts SET' in sql
|
|
@@ -601,7 +601,7 @@ def test_table_insert_chained_with_returning():
|
|
|
601
601
|
|
|
602
602
|
def test_table_update_with_where():
|
|
603
603
|
"""Test table.update() with WHERE clause"""
|
|
604
|
-
builder = Users.update(
|
|
604
|
+
builder = Users.update(username='bob_updated').where(Users.id == 5)
|
|
605
605
|
assert isinstance(builder, UpdateBuilder)
|
|
606
606
|
|
|
607
607
|
sql, params = builder.render()
|
|
@@ -615,7 +615,7 @@ def test_table_update_with_where():
|
|
|
615
615
|
|
|
616
616
|
def test_table_update_multiple_conditions():
|
|
617
617
|
"""Test table.update() with multiple WHERE conditions"""
|
|
618
|
-
builder = (Users.update(
|
|
618
|
+
builder = (Users.update(username='bob_updated', email='new@example.com')
|
|
619
619
|
.where(Users.id > 10)
|
|
620
620
|
.where(Users.created_at == None))
|
|
621
621
|
|
|
@@ -630,7 +630,7 @@ def test_table_update_multiple_conditions():
|
|
|
630
630
|
|
|
631
631
|
def test_table_update_with_returning():
|
|
632
632
|
"""Test table.update() with RETURNING"""
|
|
633
|
-
builder = Users.update(
|
|
633
|
+
builder = Users.update(username='bob_updated').where(Users.id == 5).returning()
|
|
634
634
|
sql, params = builder.render()
|
|
635
635
|
|
|
636
636
|
assert 'UPDATE users SET' in sql
|
|
@@ -681,7 +681,7 @@ def test_table_delete_with_returning():
|
|
|
681
681
|
def test_update_with_t_string_where():
|
|
682
682
|
"""Test UpdateBuilder with raw t-string WHERE clause"""
|
|
683
683
|
min_age = 18
|
|
684
|
-
builder = Users.update(
|
|
684
|
+
builder = Users.update(username='adult').where(t"age >= {min_age}")
|
|
685
685
|
|
|
686
686
|
sql, params = builder.render()
|
|
687
687
|
|
|
@@ -1062,7 +1062,7 @@ def test_update_with_onupdate_default():
|
|
|
1062
1062
|
title = SAColumn(String)
|
|
1063
1063
|
updated_at = SAColumn(String, onupdate=lambda: 'updated_timestamp')
|
|
1064
1064
|
|
|
1065
|
-
query = Articles.update(
|
|
1065
|
+
query = Articles.update(title='Updated Title').where(Articles.id == '123')
|
|
1066
1066
|
sql, params = query.render()
|
|
1067
1067
|
|
|
1068
1068
|
assert 'UPDATE articles SET' in sql
|
|
@@ -1082,7 +1082,7 @@ def test_update_overrides_onupdate():
|
|
|
1082
1082
|
title = SAColumn(String)
|
|
1083
1083
|
updated_at = SAColumn(String, onupdate=lambda: 'auto_timestamp')
|
|
1084
1084
|
|
|
1085
|
-
query = Articles.update(
|
|
1085
|
+
query = Articles.update(title='Updated', updated_at='manual_timestamp').where(Articles.id == '123')
|
|
1086
1086
|
sql, params = query.render()
|
|
1087
1087
|
|
|
1088
1088
|
assert 'UPDATE articles SET' in sql
|
|
@@ -1140,14 +1140,14 @@ def test_returning_cols_validation_update():
|
|
|
1140
1140
|
import pytest
|
|
1141
1141
|
|
|
1142
1142
|
# Test with malicious returning column
|
|
1143
|
-
query = Users.update(
|
|
1143
|
+
query = Users.update(username='hacked')
|
|
1144
1144
|
builder = query.returning("id, (SELECT password FROM admin_users LIMIT 1) AS stolen")
|
|
1145
1145
|
|
|
1146
1146
|
with pytest.raises(ValueError, match="Invalid RETURNING column name"):
|
|
1147
1147
|
builder.render()
|
|
1148
1148
|
|
|
1149
1149
|
# Test with valid returning columns (should work)
|
|
1150
|
-
query2 = Users.update(
|
|
1150
|
+
query2 = Users.update(username='updated')
|
|
1151
1151
|
builder2 = query2.where(Users.id == 5).returning("id", "username")
|
|
1152
1152
|
sql, params = builder2.render()
|
|
1153
1153
|
|
|
@@ -136,7 +136,7 @@ async def test_update_with_returning(conn):
|
|
|
136
136
|
age: int
|
|
137
137
|
|
|
138
138
|
# Update with RETURNING
|
|
139
|
-
query = TestUsers.update(
|
|
139
|
+
query = TestUsers.update(age=31).where(TestUsers.name == 'Alice').returning()
|
|
140
140
|
sql, params = query.render()
|
|
141
141
|
|
|
142
142
|
assert 'RETURNING *' in sql
|
|
@@ -196,7 +196,7 @@ async def test_update_without_returning(conn):
|
|
|
196
196
|
age: int
|
|
197
197
|
|
|
198
198
|
# Update without RETURNING
|
|
199
|
-
query = TestUsers.update(
|
|
199
|
+
query = TestUsers.update(age=31).where(TestUsers.name == 'Alice')
|
|
200
200
|
sql, params = query.render()
|
|
201
201
|
|
|
202
202
|
# Should NOT have RETURNING clause
|
|
@@ -278,7 +278,7 @@ async def test_helper_functions(conn):
|
|
|
278
278
|
assert row[1] == 25
|
|
279
279
|
|
|
280
280
|
# Test update
|
|
281
|
-
query = tsql.update('test_users',
|
|
281
|
+
query = tsql.update('test_users', 1, age=26)
|
|
282
282
|
sql, params = query.render()
|
|
283
283
|
|
|
284
284
|
await conn.execute(sql, params)
|
|
@@ -326,19 +326,23 @@ def insert(table: str, **values: Any) -> TSQL:
|
|
|
326
326
|
return TSQL(t"INSERT INTO {table:literal} {values:as_values}")
|
|
327
327
|
|
|
328
328
|
|
|
329
|
-
def update(table: str,
|
|
329
|
+
def update(table: str, id: str | int, **values: Any) -> TSQL:
|
|
330
330
|
"""Helper function to build UPDATE queries for a single row
|
|
331
331
|
|
|
332
332
|
Args:
|
|
333
333
|
table: Table name
|
|
334
|
-
values: Dictionary of column names and values to update
|
|
335
334
|
id: ID value to update
|
|
335
|
+
**values: Column names and values to update as keyword arguments
|
|
336
336
|
|
|
337
337
|
Returns:
|
|
338
338
|
TSQL object representing the UPDATE query
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
update('users', 123, name='Bob', age=35)
|
|
342
|
+
Or with dict unpacking: update('users', 123, **my_dict)
|
|
339
343
|
"""
|
|
340
|
-
if not
|
|
341
|
-
raise ValueError("
|
|
344
|
+
if not values:
|
|
345
|
+
raise ValueError("update requires at least one column value")
|
|
342
346
|
|
|
343
347
|
return TSQL(t"UPDATE {table:literal} SET {values:as_set} WHERE id = {id}")
|
|
344
348
|
|
|
@@ -298,14 +298,18 @@ class Table:
|
|
|
298
298
|
return InsertBuilder(cls, values)
|
|
299
299
|
|
|
300
300
|
@classmethod
|
|
301
|
-
def update(cls, values:
|
|
301
|
+
def update(cls, **values: Any) -> 'UpdateBuilder':
|
|
302
302
|
"""Start building an UPDATE query
|
|
303
303
|
|
|
304
304
|
Args:
|
|
305
|
-
values:
|
|
305
|
+
**values: Column names and values to update as keyword arguments
|
|
306
306
|
|
|
307
307
|
Returns:
|
|
308
308
|
UpdateBuilder for adding WHERE conditions
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
Users.update(username='bob', email='bob@example.com')
|
|
312
|
+
Or with dict unpacking: Users.update(**my_dict)
|
|
309
313
|
"""
|
|
310
314
|
return UpdateBuilder(cls, values)
|
|
311
315
|
|
t_sql-2.2.0/RELEASE_NOTES.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Release Notes
|
|
2
|
-
|
|
3
|
-
## Version 2.2.0 (2025-01-XX)
|
|
4
|
-
|
|
5
|
-
### Fixed
|
|
6
|
-
|
|
7
|
-
**Parameter Name Sanitization for NAMED/PYFORMAT Styles**
|
|
8
|
-
|
|
9
|
-
Fixed a bug where complex Python expressions in t-strings would generate syntactically invalid SQL parameter names when using NAMED (`:name`) or PYFORMAT (`%(name)s`) parameter styles.
|
|
10
|
-
|
|
11
|
-
**Problem:**
|
|
12
|
-
- Complex expressions like `{data['key']}`, `{obj.attr}`, or `{func()}` would generate invalid SQL: `:data['key']`, `:obj.attr`, `:func()`
|
|
13
|
-
- These invalid parameter names caused database errors with SQLite, PostgreSQL, and other databases that use named parameters
|
|
14
|
-
- Example: `{a + b}` would generate `:a + b`, which databases would misinterpret as column references
|
|
15
|
-
|
|
16
|
-
**Solution:**
|
|
17
|
-
- Parameter names are now sanitized to valid SQL identifiers by replacing invalid characters with underscores
|
|
18
|
-
- Simple variable names are preserved for readability: `{user_input}` → `:user_input`
|
|
19
|
-
- Complex expressions are sanitized: `{data['key']}` → `:data__key__`, `{obj.name}` → `:obj_name`
|
|
20
|
-
- Collision detection ensures unique parameter names even with edge cases
|
|
21
|
-
|
|
22
|
-
**Breaking Change:**
|
|
23
|
-
- NAMED and PYFORMAT styles now correctly return `dict` parameters instead of `list`
|
|
24
|
-
- This aligns with SQL database driver expectations (SQLite, asyncpg, etc.)
|
|
25
|
-
- If you were manually handling parameters as lists, update to use dicts:
|
|
26
|
-
```python
|
|
27
|
-
# Before (incorrect):
|
|
28
|
-
sql, params = render(query, style=NAMED)
|
|
29
|
-
# params was ['value1', 'value2'] # Wrong!
|
|
30
|
-
|
|
31
|
-
# After (correct):
|
|
32
|
-
sql, params = render(query, style=NAMED)
|
|
33
|
-
# params is {'param1': 'value1', 'param2': 'value2'}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**Impact:**
|
|
37
|
-
- Queries using NAMED/PYFORMAT styles with complex expressions now work correctly
|
|
38
|
-
- All 247 existing tests continue to pass
|
|
39
|
-
- Added 10 new tests covering parameter name edge cases
|
|
40
|
-
|
|
41
|
-
This fix ensures t-sql generates valid SQL across all parameter styles and database drivers.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|