t-sql 4.9.1__tar.gz → 4.9.3__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.1 → t_sql-4.9.3}/PKG-INFO +1 -1
  2. {t_sql-4.9.1 → t_sql-4.9.3}/pyproject.toml +1 -1
  3. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_asyncpg_integration.py +89 -1
  4. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_escaped.py +2 -2
  5. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_tsql.py +80 -0
  6. {t_sql-4.9.1 → t_sql-4.9.3}/tsql/__init__.py +3 -1
  7. {t_sql-4.9.1 → t_sql-4.9.3}/.dockerignore +0 -0
  8. {t_sql-4.9.1 → t_sql-4.9.3}/.github/workflows/publish.yml +0 -0
  9. {t_sql-4.9.1 → t_sql-4.9.3}/.github/workflows/test.yml +0 -0
  10. {t_sql-4.9.1 → t_sql-4.9.3}/.gitignore +0 -0
  11. {t_sql-4.9.1 → t_sql-4.9.3}/Dockerfile +0 -0
  12. {t_sql-4.9.1 → t_sql-4.9.3}/LICENSE +0 -0
  13. {t_sql-4.9.1 → t_sql-4.9.3}/README.md +0 -0
  14. {t_sql-4.9.1 → t_sql-4.9.3}/compose.yaml +0 -0
  15. {t_sql-4.9.1 → t_sql-4.9.3}/context7.json +0 -0
  16. {t_sql-4.9.1 → t_sql-4.9.3}/pytest.ini +0 -0
  17. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_alembic_integration.py +0 -0
  18. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_deep_nesting.py +0 -0
  19. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_different_object_types.py +0 -0
  20. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_error_messages.py +0 -0
  21. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_escaped_binary_hex.py +0 -0
  22. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_helper_functions.py +0 -0
  23. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_injection_edge_cases.py +0 -0
  24. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_injection_protection_validation.py +0 -0
  25. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_injections_for_escaped.py +0 -0
  26. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_like_patterns.py +0 -0
  27. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_mysql_integration.py +0 -0
  28. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_parameter_names.py +0 -0
  29. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_query_builder.py +0 -0
  30. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_sqlalchemy_integration.py +0 -0
  31. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_sqlite_integration.py +0 -0
  32. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_string_based_builders.py +0 -0
  33. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_styles.py +0 -0
  34. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_template_in_builders.py +0 -0
  35. {t_sql-4.9.1 → t_sql-4.9.3}/tests/test_type_processor.py +0 -0
  36. {t_sql-4.9.1 → t_sql-4.9.3}/tsql/query_builder.py +0 -0
  37. {t_sql-4.9.1 → t_sql-4.9.3}/tsql/row.py +0 -0
  38. {t_sql-4.9.1 → t_sql-4.9.3}/tsql/styles.py +0 -0
  39. {t_sql-4.9.1 → t_sql-4.9.3}/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.1
3
+ Version: 4.9.3
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.1"
7
+ version = "4.9.3"
8
8
  description = "Safe SQL. SQL queries for python t-strings (PEP 750)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -369,4 +369,92 @@ async def test_like_pattern_format_specs_with_postgres(conn):
369
369
  rows = await conn.fetch(sql, *params)
370
370
 
371
371
  # Should match both john_doe and john%smith (case-insensitive)
372
- assert len(rows) == 2
372
+ assert len(rows) == 2
373
+
374
+
375
+ async def test_jsonb_with_dict(conn):
376
+ """Test that dict objects can be used with asyncpg JSONB columns via set_type_codec"""
377
+ import json
378
+
379
+ # Create a table with JSONB column
380
+ await conn.execute("""
381
+ CREATE TABLE IF NOT EXISTS test_jsonb (
382
+ id SERIAL PRIMARY KEY,
383
+ data JSONB
384
+ )
385
+ """)
386
+
387
+ try:
388
+ # Set up a codec so asyncpg can handle Python dicts for JSONB
389
+ # This is the recommended asyncpg pattern for JSONB
390
+ await conn.set_type_codec(
391
+ 'jsonb',
392
+ encoder=json.dumps,
393
+ decoder=json.loads,
394
+ schema='pg_catalog'
395
+ )
396
+
397
+ # Now dicts work directly because tsql preserves them (not stringified)
398
+ my_dict = {'name': 'billy', 'age': 30, 'tags': ['admin', 'user']}
399
+
400
+ query, params = tsql.render(
401
+ t"INSERT INTO test_jsonb (data) VALUES ({my_dict})",
402
+ style=tsql.styles.NUMERIC_DOLLAR
403
+ )
404
+
405
+ # Verify the parameter is still a dict (not stringified)
406
+ assert len(params) == 1
407
+ assert isinstance(params[0], dict)
408
+
409
+ # This should work now with the type codec
410
+ await conn.execute(query, *params)
411
+
412
+ # Verify data was inserted correctly and can be queried
413
+ row = await conn.fetchrow("SELECT data FROM test_jsonb WHERE id = 1")
414
+ assert row['data'] == my_dict
415
+ assert row['data']['name'] == 'billy'
416
+ assert row['data']['tags'] == ['admin', 'user']
417
+
418
+ # Test querying with JSONB operators
419
+ name = 'billy'
420
+ query2, params2 = tsql.render(
421
+ t"SELECT data FROM test_jsonb WHERE data->>'name' = {name}",
422
+ style=tsql.styles.NUMERIC_DOLLAR
423
+ )
424
+ rows = await conn.fetch(query2, *params2)
425
+ assert len(rows) == 1
426
+ finally:
427
+ await conn.execute("DROP TABLE IF EXISTS test_jsonb")
428
+
429
+
430
+ async def test_array_with_list(conn):
431
+ """Test that list objects work correctly with asyncpg array columns"""
432
+ # Create a table with array column
433
+ await conn.execute("""
434
+ CREATE TABLE IF NOT EXISTS test_array (
435
+ id SERIAL PRIMARY KEY,
436
+ tags TEXT[]
437
+ )
438
+ """)
439
+
440
+ try:
441
+ # Insert a list - this should work because list is preserved
442
+ my_list = ['tag1', 'tag2', 'tag3']
443
+
444
+ query, params = tsql.render(
445
+ t"INSERT INTO test_array (tags) VALUES ({my_list})",
446
+ style=tsql.styles.NUMERIC_DOLLAR
447
+ )
448
+
449
+ # Verify the parameter is still a list (not stringified)
450
+ assert len(params) == 1
451
+ assert isinstance(params[0], list)
452
+
453
+ # This should work without asyncpg throwing an error
454
+ await conn.execute(query, *params)
455
+
456
+ # Verify data was inserted correctly
457
+ row = await conn.fetchrow("SELECT tags FROM test_array WHERE id = 1")
458
+ assert row['tags'] == my_list
459
+ finally:
460
+ await conn.execute("DROP TABLE IF EXISTS test_array")
@@ -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
 
@@ -168,4 +168,84 @@ def test_datetime_with_format_spec_converts_to_string():
168
168
  assert isinstance(result[1][0], str)
169
169
 
170
170
 
171
+ def test_dict_preserved_as_native_type():
172
+ """Dict values should pass through as dict objects, not strings."""
173
+ my_dict = {'name': 'billy', 'age': 30}
174
+ result = tsql.render(t'INSERT INTO users (data) VALUES ({my_dict})')
175
+ assert result.sql == 'INSERT INTO users (data) VALUES (?)'
176
+ assert result.values == [{'name': 'billy', 'age': 30}]
177
+ assert isinstance(result.values[0], dict)
178
+
179
+
180
+ def test_list_preserved_as_native_type():
181
+ """List values should pass through as list objects, not strings."""
182
+ my_list = [1, 2, 3, 'four']
183
+ result = tsql.render(t'INSERT INTO items (tags) VALUES ({my_list})')
184
+ assert result.sql == 'INSERT INTO items (tags) VALUES (?)'
185
+ assert result.values == [[1, 2, 3, 'four']]
186
+ assert isinstance(result.values[0], list)
187
+
188
+
189
+ def test_set_preserved_as_native_type():
190
+ """Set values should pass through as set objects, not strings."""
191
+ my_set = {1, 2, 3}
192
+ result = tsql.render(t'INSERT INTO items (ids) VALUES ({my_set})')
193
+ assert result.sql == 'INSERT INTO items (ids) VALUES (?)'
194
+ assert isinstance(result.values[0], set)
195
+ assert result.values[0] == {1, 2, 3}
196
+
197
+
198
+ def test_nested_dict_with_list():
199
+ """Nested structures should preserve their types."""
200
+ data = {'tags': ['a', 'b'], 'count': 5}
201
+ result = tsql.render(t'UPDATE users SET meta = {data}')
202
+ assert isinstance(result.values[0], dict)
203
+ assert isinstance(result.values[0]['tags'], list)
204
+
205
+
206
+ def test_empty_collections_preserved():
207
+ """Empty collections should also preserve their types."""
208
+ empty_dict = {}
209
+ empty_list = []
210
+ empty_set = set()
211
+
212
+ r1 = tsql.render(t'VALUES ({empty_dict})')
213
+ r2 = tsql.render(t'VALUES ({empty_list})')
214
+ r3 = tsql.render(t'VALUES ({empty_set})')
215
+
216
+ assert isinstance(r1.values[0], dict)
217
+ assert isinstance(r2.values[0], list)
218
+ assert isinstance(r3.values[0], set)
219
+
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
+
171
251
 
@@ -193,9 +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():
196
+ case '', _:
197
+ # No format spec - pass through value as-is for database driver to handle
197
198
  return [Parameter(val.expression, value)]
198
199
  case _, _:
200
+ # Has format spec - apply formatting (e.g., {dt:%Y-%m-%d})
199
201
  return [Parameter(val.expression, formatter.format_field(value, val.format_spec))]
200
202
 
201
203
  if isinstance(val, Template):
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