t-sql 4.9.1__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.
- {t_sql-4.9.1 → t_sql-4.9.2}/PKG-INFO +1 -1
- {t_sql-4.9.1 → t_sql-4.9.2}/pyproject.toml +1 -1
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_asyncpg_integration.py +89 -1
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_tsql.py +50 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tsql/__init__.py +2 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/.dockerignore +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/.github/workflows/publish.yml +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/.github/workflows/test.yml +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/.gitignore +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/Dockerfile +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/LICENSE +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/README.md +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/compose.yaml +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/context7.json +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/pytest.ini +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_alembic_integration.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_deep_nesting.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_different_object_types.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_error_messages.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_escaped.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_helper_functions.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_like_patterns.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_mysql_integration.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_parameter_names.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_query_builder.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_sqlite_integration.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_string_based_builders.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_styles.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_template_in_builders.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tests/test_type_processor.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tsql/query_builder.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tsql/row.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tsql/styles.py +0 -0
- {t_sql-4.9.1 → t_sql-4.9.2}/tsql/type_processor.py +0 -0
|
@@ -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")
|
|
@@ -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
|
|
|
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
|
|
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
|