t-sql 4.9.0__tar.gz → 4.9.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.
Files changed (39) hide show
  1. {t_sql-4.9.0 → t_sql-4.9.2}/PKG-INFO +1 -1
  2. {t_sql-4.9.0 → t_sql-4.9.2}/pyproject.toml +1 -1
  3. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_asyncpg_integration.py +89 -1
  4. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_query_builder.py +13 -0
  5. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_tsql.py +50 -0
  6. {t_sql-4.9.0 → t_sql-4.9.2}/tsql/__init__.py +2 -0
  7. {t_sql-4.9.0 → t_sql-4.9.2}/tsql/query_builder.py +7 -1
  8. {t_sql-4.9.0 → t_sql-4.9.2}/.dockerignore +0 -0
  9. {t_sql-4.9.0 → t_sql-4.9.2}/.github/workflows/publish.yml +0 -0
  10. {t_sql-4.9.0 → t_sql-4.9.2}/.github/workflows/test.yml +0 -0
  11. {t_sql-4.9.0 → t_sql-4.9.2}/.gitignore +0 -0
  12. {t_sql-4.9.0 → t_sql-4.9.2}/Dockerfile +0 -0
  13. {t_sql-4.9.0 → t_sql-4.9.2}/LICENSE +0 -0
  14. {t_sql-4.9.0 → t_sql-4.9.2}/README.md +0 -0
  15. {t_sql-4.9.0 → t_sql-4.9.2}/compose.yaml +0 -0
  16. {t_sql-4.9.0 → t_sql-4.9.2}/context7.json +0 -0
  17. {t_sql-4.9.0 → t_sql-4.9.2}/pytest.ini +0 -0
  18. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_alembic_integration.py +0 -0
  19. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_deep_nesting.py +0 -0
  20. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_different_object_types.py +0 -0
  21. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_error_messages.py +0 -0
  22. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_escaped.py +0 -0
  23. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_escaped_binary_hex.py +0 -0
  24. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_helper_functions.py +0 -0
  25. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_injection_edge_cases.py +0 -0
  26. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_injection_protection_validation.py +0 -0
  27. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_injections_for_escaped.py +0 -0
  28. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_like_patterns.py +0 -0
  29. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_mysql_integration.py +0 -0
  30. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_parameter_names.py +0 -0
  31. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_sqlalchemy_integration.py +0 -0
  32. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_sqlite_integration.py +0 -0
  33. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_string_based_builders.py +0 -0
  34. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_styles.py +0 -0
  35. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_template_in_builders.py +0 -0
  36. {t_sql-4.9.0 → t_sql-4.9.2}/tests/test_type_processor.py +0 -0
  37. {t_sql-4.9.0 → t_sql-4.9.2}/tsql/row.py +0 -0
  38. {t_sql-4.9.0 → t_sql-4.9.2}/tsql/styles.py +0 -0
  39. {t_sql-4.9.0 → t_sql-4.9.2}/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.0
3
+ Version: 4.9.2
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.0"
7
+ version = "4.9.2"
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")
@@ -116,6 +116,19 @@ def test_select_specific_columns():
116
116
  assert params == []
117
117
 
118
118
 
119
+ def test_multiple_select_calls_append():
120
+ """Test that multiple .select() calls append columns instead of replacing them"""
121
+ query = Users.select(Users.id)
122
+ query.select(Users.username, Users.email)
123
+ sql, params = query.render()
124
+
125
+ # All three columns should be present
126
+ assert 'users.id' in sql
127
+ assert 'users.username' in sql
128
+ assert 'users.email' in sql
129
+ assert params == []
130
+
131
+
119
132
  def test_select_with_where():
120
133
  """Test SELECT with WHERE clause"""
121
134
  query = Users.select(Users.id, Users.username).where(Users.id == 5)
@@ -168,4 +168,54 @@ 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
+
171
221
 
@@ -195,6 +195,8 @@ class TSQL:
195
195
  return [Parameter(val.expression, val.value)]
196
196
  case '', datetime.datetime() | datetime.date() | datetime.time() | datetime.timedelta():
197
197
  return [Parameter(val.expression, value)]
198
+ case '', dict() | list() | set():
199
+ return [Parameter(val.expression, value)]
198
200
  case _, _:
199
201
  return [Parameter(val.expression, formatter.format_field(value, val.format_spec))]
200
202
 
@@ -1201,7 +1201,13 @@ class SelectQueryBuilder(QueryBuilder):
1201
1201
  # No columns specified selects all (SELECT *)
1202
1202
  users.select()
1203
1203
  """
1204
- self._columns = list(columns) if columns else None
1204
+ if columns:
1205
+ if self._columns is None:
1206
+ self._columns = []
1207
+ self._columns.extend(columns)
1208
+ else:
1209
+ # Calling select() with no args resets to SELECT *
1210
+ self._columns = None
1205
1211
  return self
1206
1212
 
1207
1213
  def where(self, condition: Union[Condition, Template]) -> 'SelectQueryBuilder':
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