t-sql 4.9.2__tar.gz → 4.10.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.
Files changed (39) hide show
  1. {t_sql-4.9.2 → t_sql-4.10.0}/PKG-INFO +1 -1
  2. {t_sql-4.9.2 → t_sql-4.10.0}/pyproject.toml +1 -1
  3. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_escaped.py +2 -2
  4. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_query_builder.py +42 -0
  5. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_tsql.py +30 -0
  6. {t_sql-4.9.2 → t_sql-4.10.0}/tsql/__init__.py +3 -3
  7. {t_sql-4.9.2 → t_sql-4.10.0}/tsql/query_builder.py +12 -3
  8. {t_sql-4.9.2 → t_sql-4.10.0}/.dockerignore +0 -0
  9. {t_sql-4.9.2 → t_sql-4.10.0}/.github/workflows/publish.yml +0 -0
  10. {t_sql-4.9.2 → t_sql-4.10.0}/.github/workflows/test.yml +0 -0
  11. {t_sql-4.9.2 → t_sql-4.10.0}/.gitignore +0 -0
  12. {t_sql-4.9.2 → t_sql-4.10.0}/Dockerfile +0 -0
  13. {t_sql-4.9.2 → t_sql-4.10.0}/LICENSE +0 -0
  14. {t_sql-4.9.2 → t_sql-4.10.0}/README.md +0 -0
  15. {t_sql-4.9.2 → t_sql-4.10.0}/compose.yaml +0 -0
  16. {t_sql-4.9.2 → t_sql-4.10.0}/context7.json +0 -0
  17. {t_sql-4.9.2 → t_sql-4.10.0}/pytest.ini +0 -0
  18. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_alembic_integration.py +0 -0
  19. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_asyncpg_integration.py +0 -0
  20. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_deep_nesting.py +0 -0
  21. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_different_object_types.py +0 -0
  22. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_error_messages.py +0 -0
  23. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_escaped_binary_hex.py +0 -0
  24. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_helper_functions.py +0 -0
  25. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_injection_edge_cases.py +0 -0
  26. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_injection_protection_validation.py +0 -0
  27. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_injections_for_escaped.py +0 -0
  28. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_like_patterns.py +0 -0
  29. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_mysql_integration.py +0 -0
  30. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_parameter_names.py +0 -0
  31. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_sqlalchemy_integration.py +0 -0
  32. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_sqlite_integration.py +0 -0
  33. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_string_based_builders.py +0 -0
  34. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_styles.py +0 -0
  35. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_template_in_builders.py +0 -0
  36. {t_sql-4.9.2 → t_sql-4.10.0}/tests/test_type_processor.py +0 -0
  37. {t_sql-4.9.2 → t_sql-4.10.0}/tsql/row.py +0 -0
  38. {t_sql-4.9.2 → t_sql-4.10.0}/tsql/styles.py +0 -0
  39. {t_sql-4.9.2 → t_sql-4.10.0}/tsql/type_processor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t-sql
3
- Version: 4.9.2
3
+ Version: 4.10.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "t-sql"
7
- version = "4.9.2"
7
+ version = "4.10.0"
8
8
  description = "Safe SQL. SQL queries for python t-strings (PEP 750)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -123,8 +123,8 @@ def test_escaped_handles_float_values():
123
123
  """Test proper escaping of float values"""
124
124
  price = 19.99
125
125
  result = tsql.render(t"SELECT * FROM products WHERE price = {price}", style=tsql.styles.ESCAPED)
126
- # Note: floats get converted to strings by the formatter before reaching ESCAPED style
127
- assert result[0] == "SELECT * FROM products WHERE price = '19.99'"
126
+ # Floats are passed through and rendered as numeric (no quotes) - correct SQL behavior
127
+ assert result[0] == "SELECT * FROM products WHERE price = 19.99"
128
128
  assert result[1] == []
129
129
 
130
130
 
@@ -1545,3 +1545,45 @@ def test_multiple_recursive_ctes():
1545
1545
  assert sql.startswith("WITH RECURSIVE")
1546
1546
  assert "normal AS" in sql
1547
1547
  assert "tree AS" in sql
1548
+
1549
+
1550
+ def test_where_with_bare_boolean_column():
1551
+ """Test WHERE with bare boolean column (truthy check)"""
1552
+ class Contacts(Table):
1553
+ id: Column
1554
+ is_primary: Column
1555
+
1556
+ query = Contacts.select().where(Contacts.is_primary)
1557
+ sql, params = query.render()
1558
+
1559
+ assert 'WHERE contacts.is_primary' in sql
1560
+ assert params == []
1561
+
1562
+
1563
+ def test_update_where_with_bare_boolean_column():
1564
+ """Test UPDATE WHERE with bare boolean column"""
1565
+ class Contacts(Table):
1566
+ id: Column
1567
+ is_primary: Column
1568
+ name: Column
1569
+
1570
+ query = Contacts.update(name='updated').where(Contacts.is_primary)
1571
+ sql, params = query.render()
1572
+
1573
+ assert 'UPDATE contacts SET' in sql
1574
+ assert 'WHERE contacts.is_primary' in sql
1575
+ assert params == ['updated']
1576
+
1577
+
1578
+ def test_delete_where_with_bare_boolean_column():
1579
+ """Test DELETE WHERE with bare boolean column"""
1580
+ class Contacts(Table):
1581
+ id: Column
1582
+ is_primary: Column
1583
+
1584
+ query = Contacts.delete().where(Contacts.is_primary)
1585
+ sql, params = query.render()
1586
+
1587
+ assert 'DELETE FROM contacts' in sql
1588
+ assert 'WHERE contacts.is_primary' in sql
1589
+ assert params == []
@@ -218,4 +218,34 @@ def test_empty_collections_preserved():
218
218
  assert isinstance(r3.values[0], set)
219
219
 
220
220
 
221
+ def test_custom_object_preserved():
222
+ """Custom objects should pass through for database drivers with custom codecs."""
223
+ class Point:
224
+ def __init__(self, x, y):
225
+ self.x = x
226
+ self.y = y
227
+
228
+ p = Point(3, 4)
229
+ result = tsql.render(t'INSERT INTO coords (loc) VALUES ({p})')
230
+ assert result.sql == 'INSERT INTO coords (loc) VALUES (?)'
231
+ assert isinstance(result.values[0], Point)
232
+ assert result.values[0].x == 3
233
+ assert result.values[0].y == 4
234
+
235
+
236
+ def test_custom_object_with_format_spec_stringifies():
237
+ """Custom objects WITH a format spec should be formatted (stringified)."""
238
+ class Point:
239
+ def __init__(self, x, y):
240
+ self.x = x
241
+ self.y = y
242
+ def __format__(self, spec):
243
+ return f"POINT({self.x},{self.y})"
244
+
245
+ p = Point(3, 4)
246
+ result = tsql.render(t'INSERT INTO coords (loc) VALUES ({p:s})')
247
+ assert result.values[0] == "POINT(3,4)"
248
+ assert isinstance(result.values[0], str)
249
+
250
+
221
251
 
@@ -193,11 +193,11 @@ class TSQL:
193
193
  return [Parameter(val.expression, value)]
194
194
  case _, int():
195
195
  return [Parameter(val.expression, val.value)]
196
- case '', datetime.datetime() | datetime.date() | datetime.time() | datetime.timedelta():
197
- return [Parameter(val.expression, value)]
198
- case '', dict() | list() | set():
196
+ case '', _:
197
+ # No format spec - pass through value as-is for database driver to handle
199
198
  return [Parameter(val.expression, value)]
200
199
  case _, _:
200
+ # Has format spec - apply formatting (e.g., {dt:%Y-%m-%d})
201
201
  return [Parameter(val.expression, formatter.format_field(value, val.format_spec))]
202
202
 
203
203
  if isinstance(val, Template):
@@ -932,7 +932,7 @@ class UpdateBuilder(QueryBuilder):
932
932
  string_table = _StringTable(table_name, schema)
933
933
  return cls(string_table, values)
934
934
 
935
- def where(self, condition: Union[Condition, Template]) -> 'UpdateBuilder':
935
+ def where(self, condition: Union[Condition, Template, Column]) -> 'UpdateBuilder':
936
936
  """Add a WHERE condition (multiple calls are ANDed together)"""
937
937
  self._conditions.append(condition)
938
938
  self._requires_where = False
@@ -994,6 +994,9 @@ class UpdateBuilder(QueryBuilder):
994
994
  for cond in self._conditions:
995
995
  if isinstance(cond, Template):
996
996
  where_parts.append(t'({cond})')
997
+ elif isinstance(cond, Column):
998
+ col_str = str(cond)
999
+ where_parts.append(t'{col_str:literal}')
997
1000
  else:
998
1001
  where_parts.append(cond.to_tsql())
999
1002
  combined_where = t_join(t' AND ', where_parts)
@@ -1057,7 +1060,7 @@ class DeleteBuilder(QueryBuilder):
1057
1060
  string_table = _StringTable(table_name, schema)
1058
1061
  return cls(string_table)
1059
1062
 
1060
- def where(self, condition: Union[Condition, Template]) -> 'DeleteBuilder':
1063
+ def where(self, condition: Union[Condition, Template, Column]) -> 'DeleteBuilder':
1061
1064
  """Add a WHERE condition (multiple calls are ANDed together)"""
1062
1065
  self._conditions.append(condition)
1063
1066
  self._requires_where = False
@@ -1112,6 +1115,9 @@ class DeleteBuilder(QueryBuilder):
1112
1115
  for cond in self._conditions:
1113
1116
  if isinstance(cond, Template):
1114
1117
  where_parts.append(t'({cond})')
1118
+ elif isinstance(cond, Column):
1119
+ col_str = str(cond)
1120
+ where_parts.append(t'{col_str:literal}')
1115
1121
  else:
1116
1122
  where_parts.append(cond.to_tsql())
1117
1123
  combined_where = t_join(t' AND ', where_parts)
@@ -1210,7 +1216,7 @@ class SelectQueryBuilder(QueryBuilder):
1210
1216
  self._columns = None
1211
1217
  return self
1212
1218
 
1213
- def where(self, condition: Union[Condition, Template]) -> 'SelectQueryBuilder':
1219
+ def where(self, condition: Union[Condition, Template, Column]) -> 'SelectQueryBuilder':
1214
1220
  """Add a WHERE condition (multiple calls are ANDed together)
1215
1221
 
1216
1222
  Accepts either Condition objects from query builder or raw t-string Templates
@@ -1401,6 +1407,9 @@ class SelectQueryBuilder(QueryBuilder):
1401
1407
  for cond in self._conditions:
1402
1408
  if isinstance(cond, Template):
1403
1409
  where_parts.append(t'({cond})')
1410
+ elif isinstance(cond, Column):
1411
+ col_str = str(cond)
1412
+ where_parts.append(t'{col_str:literal}')
1404
1413
  else:
1405
1414
  where_parts.append(cond.to_tsql())
1406
1415
  combined_where = t_join(t' AND ', where_parts)
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