t-sql 4.9.3__tar.gz → 4.11.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-4.9.3/README.md → t_sql-4.11.0/PKG-INFO +18 -0
- t_sql-4.9.3/PKG-INFO → t_sql-4.11.0/README.md +8 -10
- {t_sql-4.9.3 → t_sql-4.11.0}/pyproject.toml +1 -1
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_asyncpg_integration.py +32 -1
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_query_builder.py +79 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_sqlite_integration.py +26 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_tsql.py +1 -1
- {t_sql-4.9.3 → t_sql-4.11.0}/tsql/query_builder.py +19 -5
- {t_sql-4.9.3 → t_sql-4.11.0}/.dockerignore +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/.github/workflows/publish.yml +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/.github/workflows/test.yml +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/.gitignore +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/Dockerfile +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/LICENSE +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/compose.yaml +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/context7.json +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/pytest.ini +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_alembic_integration.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_deep_nesting.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_different_object_types.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_error_messages.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_escaped.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_helper_functions.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_like_patterns.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_mysql_integration.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_parameter_names.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_string_based_builders.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_styles.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_template_in_builders.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tests/test_type_processor.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tsql/__init__.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tsql/row.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tsql/styles.py +0 -0
- {t_sql-4.9.3 → t_sql-4.11.0}/tsql/type_processor.py +0 -0
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: t-sql
|
|
3
|
+
Version: 4.11.0
|
|
4
|
+
Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
|
|
5
|
+
Project-URL: Homepage, https://github.com/nhumrich/t-sql
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Requires-Dist: alembic>=1.17.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
1
11
|
# t-sql
|
|
2
12
|
|
|
3
13
|
A lightweight SQL templating library that leverages Python 3.14's t-strings (PEP 750).
|
|
@@ -375,6 +385,14 @@ query = Users.insert(id='abc123', username='john', email='john@example.com').ret
|
|
|
375
385
|
sql, params = query.render()
|
|
376
386
|
# ('INSERT INTO users (id, username, email) VALUES (?, ?, ?) RETURNING *', [...])
|
|
377
387
|
|
|
388
|
+
# INSERT with all column defaults — no values provided (Postgres/SQLite)
|
|
389
|
+
# Emits `DEFAULT VALUES`; useful when every column has a DB/SA default.
|
|
390
|
+
# Note: MySQL does not support `DEFAULT VALUES` syntax. For MySQL, provide at
|
|
391
|
+
# least one column or use raw SQL (`INSERT INTO t () VALUES ()`).
|
|
392
|
+
query = Users.insert().returning('id')
|
|
393
|
+
sql, params = query.render()
|
|
394
|
+
# ('INSERT INTO users DEFAULT VALUES RETURNING id', [])
|
|
395
|
+
|
|
378
396
|
# INSERT IGNORE (MySQL)
|
|
379
397
|
query = Users.insert(id='abc123', username='john', email='john@example.com').ignore()
|
|
380
398
|
sql, params = query.render()
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: t-sql
|
|
3
|
-
Version: 4.9.3
|
|
4
|
-
Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
|
|
5
|
-
Project-URL: Homepage, https://github.com/nhumrich/t-sql
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.14
|
|
8
|
-
Requires-Dist: alembic>=1.17.0
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
|
|
11
1
|
# t-sql
|
|
12
2
|
|
|
13
3
|
A lightweight SQL templating library that leverages Python 3.14's t-strings (PEP 750).
|
|
@@ -385,6 +375,14 @@ query = Users.insert(id='abc123', username='john', email='john@example.com').ret
|
|
|
385
375
|
sql, params = query.render()
|
|
386
376
|
# ('INSERT INTO users (id, username, email) VALUES (?, ?, ?) RETURNING *', [...])
|
|
387
377
|
|
|
378
|
+
# INSERT with all column defaults — no values provided (Postgres/SQLite)
|
|
379
|
+
# Emits `DEFAULT VALUES`; useful when every column has a DB/SA default.
|
|
380
|
+
# Note: MySQL does not support `DEFAULT VALUES` syntax. For MySQL, provide at
|
|
381
|
+
# least one column or use raw SQL (`INSERT INTO t () VALUES ()`).
|
|
382
|
+
query = Users.insert().returning('id')
|
|
383
|
+
sql, params = query.render()
|
|
384
|
+
# ('INSERT INTO users DEFAULT VALUES RETURNING id', [])
|
|
385
|
+
|
|
388
386
|
# INSERT IGNORE (MySQL)
|
|
389
387
|
query = Users.insert(id='abc123', username='john', email='john@example.com').ignore()
|
|
390
388
|
sql, params = query.render()
|
|
@@ -457,4 +457,35 @@ async def test_array_with_list(conn):
|
|
|
457
457
|
row = await conn.fetchrow("SELECT tags FROM test_array WHERE id = 1")
|
|
458
458
|
assert row['tags'] == my_list
|
|
459
459
|
finally:
|
|
460
|
-
await conn.execute("DROP TABLE IF EXISTS test_array")
|
|
460
|
+
await conn.execute("DROP TABLE IF EXISTS test_array")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
async def test_insert_all_defaults_with_returning(conn):
|
|
464
|
+
"""Insert with empty values uses DEFAULT VALUES; all columns have defaults."""
|
|
465
|
+
from tsql.query_builder import Table, Column
|
|
466
|
+
|
|
467
|
+
await conn.execute("""
|
|
468
|
+
CREATE TABLE IF NOT EXISTS test_defaults (
|
|
469
|
+
id SERIAL PRIMARY KEY,
|
|
470
|
+
status VARCHAR(20) DEFAULT 'draft',
|
|
471
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
472
|
+
)
|
|
473
|
+
""")
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
class TestDefaults(Table, table_name='test_defaults'):
|
|
477
|
+
id: Column
|
|
478
|
+
status: Column
|
|
479
|
+
created_at: Column
|
|
480
|
+
|
|
481
|
+
query, params = TestDefaults.insert().returning('id', 'status').render(
|
|
482
|
+
style=tsql.styles.NUMERIC_DOLLAR
|
|
483
|
+
)
|
|
484
|
+
assert query == 'INSERT INTO test_defaults DEFAULT VALUES RETURNING id, status'
|
|
485
|
+
assert params == []
|
|
486
|
+
|
|
487
|
+
row = await conn.fetchrow(query, *params)
|
|
488
|
+
assert row['id'] is not None
|
|
489
|
+
assert row['status'] == 'draft'
|
|
490
|
+
finally:
|
|
491
|
+
await conn.execute("DROP TABLE IF EXISTS test_defaults")
|
|
@@ -1208,6 +1208,43 @@ def test_insert_without_metadata_ignores_defaults():
|
|
|
1208
1208
|
assert params == ['1', 'test']
|
|
1209
1209
|
|
|
1210
1210
|
|
|
1211
|
+
class _DefaultsTable(Table, table_name='mytable'):
|
|
1212
|
+
id: Column
|
|
1213
|
+
name: Column
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def test_insert_all_defaults():
|
|
1217
|
+
sql, params = _DefaultsTable.insert().render()
|
|
1218
|
+
assert sql == 'INSERT INTO mytable DEFAULT VALUES'
|
|
1219
|
+
assert params == []
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def test_insert_all_defaults_with_returning():
|
|
1223
|
+
sql, params = _DefaultsTable.insert().returning('id').render()
|
|
1224
|
+
assert sql == 'INSERT INTO mytable DEFAULT VALUES RETURNING id'
|
|
1225
|
+
assert params == []
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def test_insert_all_defaults_with_returning_star():
|
|
1229
|
+
sql, params = _DefaultsTable.insert().returning().render()
|
|
1230
|
+
assert sql == 'INSERT INTO mytable DEFAULT VALUES RETURNING *'
|
|
1231
|
+
assert params == []
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def test_insert_all_defaults_with_on_conflict_do_nothing():
|
|
1235
|
+
sql, params = _DefaultsTable.insert().on_conflict_do_nothing().render()
|
|
1236
|
+
assert sql == 'INSERT INTO mytable DEFAULT VALUES ON CONFLICT DO NOTHING'
|
|
1237
|
+
assert params == []
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
def test_insert_all_defaults_mysql_on_duplicate_raises():
|
|
1241
|
+
import pytest
|
|
1242
|
+
|
|
1243
|
+
builder = _DefaultsTable.insert().on_duplicate_key_update()
|
|
1244
|
+
with pytest.raises(ValueError, match='DEFAULT VALUES'):
|
|
1245
|
+
builder.render()
|
|
1246
|
+
|
|
1247
|
+
|
|
1211
1248
|
def test_update_with_onupdate_default():
|
|
1212
1249
|
"""Test that onupdate defaults are applied during UPDATE"""
|
|
1213
1250
|
from sqlalchemy import MetaData, Column as SAColumn, String
|
|
@@ -1545,3 +1582,45 @@ def test_multiple_recursive_ctes():
|
|
|
1545
1582
|
assert sql.startswith("WITH RECURSIVE")
|
|
1546
1583
|
assert "normal AS" in sql
|
|
1547
1584
|
assert "tree AS" in sql
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def test_where_with_bare_boolean_column():
|
|
1588
|
+
"""Test WHERE with bare boolean column (truthy check)"""
|
|
1589
|
+
class Contacts(Table):
|
|
1590
|
+
id: Column
|
|
1591
|
+
is_primary: Column
|
|
1592
|
+
|
|
1593
|
+
query = Contacts.select().where(Contacts.is_primary)
|
|
1594
|
+
sql, params = query.render()
|
|
1595
|
+
|
|
1596
|
+
assert 'WHERE contacts.is_primary' in sql
|
|
1597
|
+
assert params == []
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def test_update_where_with_bare_boolean_column():
|
|
1601
|
+
"""Test UPDATE WHERE with bare boolean column"""
|
|
1602
|
+
class Contacts(Table):
|
|
1603
|
+
id: Column
|
|
1604
|
+
is_primary: Column
|
|
1605
|
+
name: Column
|
|
1606
|
+
|
|
1607
|
+
query = Contacts.update(name='updated').where(Contacts.is_primary)
|
|
1608
|
+
sql, params = query.render()
|
|
1609
|
+
|
|
1610
|
+
assert 'UPDATE contacts SET' in sql
|
|
1611
|
+
assert 'WHERE contacts.is_primary' in sql
|
|
1612
|
+
assert params == ['updated']
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
def test_delete_where_with_bare_boolean_column():
|
|
1616
|
+
"""Test DELETE WHERE with bare boolean column"""
|
|
1617
|
+
class Contacts(Table):
|
|
1618
|
+
id: Column
|
|
1619
|
+
is_primary: Column
|
|
1620
|
+
|
|
1621
|
+
query = Contacts.delete().where(Contacts.is_primary)
|
|
1622
|
+
sql, params = query.render()
|
|
1623
|
+
|
|
1624
|
+
assert 'DELETE FROM contacts' in sql
|
|
1625
|
+
assert 'WHERE contacts.is_primary' in sql
|
|
1626
|
+
assert params == []
|
|
@@ -429,3 +429,29 @@ async def test_like_pattern_format_specs(conn):
|
|
|
429
429
|
# Should match john_doe
|
|
430
430
|
assert len(rows) == 1
|
|
431
431
|
assert rows[0][0] == 'john_doe'
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
async def test_insert_all_defaults_with_returning(conn):
|
|
435
|
+
"""Insert with empty values uses DEFAULT VALUES on SQLite."""
|
|
436
|
+
from tsql.query_builder import Column
|
|
437
|
+
|
|
438
|
+
await conn.execute("""
|
|
439
|
+
CREATE TABLE test_defaults (
|
|
440
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
441
|
+
status TEXT DEFAULT 'draft'
|
|
442
|
+
)
|
|
443
|
+
""")
|
|
444
|
+
await conn.commit()
|
|
445
|
+
|
|
446
|
+
class TestDefaults(Table, table_name='test_defaults'):
|
|
447
|
+
id: Column
|
|
448
|
+
status: Column
|
|
449
|
+
|
|
450
|
+
sql, params = TestDefaults.insert().returning('id', 'status').render()
|
|
451
|
+
assert sql == 'INSERT INTO test_defaults DEFAULT VALUES RETURNING id, status'
|
|
452
|
+
assert params == []
|
|
453
|
+
|
|
454
|
+
cursor = await conn.execute(sql, params)
|
|
455
|
+
row = await cursor.fetchone()
|
|
456
|
+
assert row[0] is not None
|
|
457
|
+
assert row[1] == 'draft'
|
|
@@ -69,7 +69,7 @@ def test_correct_final_query_with_literals():
|
|
|
69
69
|
def test_disallows_bad_literals():
|
|
70
70
|
table = "users'"
|
|
71
71
|
col = "name"
|
|
72
|
-
with pytest.raises(ValueError):
|
|
72
|
+
with pytest.raises(ValueError):
|
|
73
73
|
result = tsql.render(t'select id, {col:literal} from {table:literal}')
|
|
74
74
|
|
|
75
75
|
|
|
@@ -759,8 +759,9 @@ class InsertBuilder(QueryBuilder):
|
|
|
759
759
|
processor = self.base_table._type_processors.get(col_name)
|
|
760
760
|
values_dict[col_name] = _process_value_for_builder(value, processor)
|
|
761
761
|
|
|
762
|
-
|
|
763
|
-
|
|
762
|
+
if not values_dict:
|
|
763
|
+
parts.append(t'INSERT INTO {table_name:literal} DEFAULT VALUES')
|
|
764
|
+
elif self._ignore:
|
|
764
765
|
parts.append(t'INSERT IGNORE INTO {table_name:literal} {values_dict:as_values}')
|
|
765
766
|
else:
|
|
766
767
|
parts.append(t'INSERT INTO {table_name:literal} {values_dict:as_values}')
|
|
@@ -821,6 +822,10 @@ class InsertBuilder(QueryBuilder):
|
|
|
821
822
|
update_dict[col_name] = _process_value_for_builder(value, processor)
|
|
822
823
|
parts.append(t'ON DUPLICATE KEY UPDATE {update_dict:as_set}')
|
|
823
824
|
else:
|
|
825
|
+
if not self.values:
|
|
826
|
+
raise ValueError(
|
|
827
|
+
"ON DUPLICATE KEY UPDATE requires explicit columns when inserting DEFAULT VALUES"
|
|
828
|
+
)
|
|
824
829
|
# Default: update all columns with alias.column (new MySQL syntax)
|
|
825
830
|
update_parts = []
|
|
826
831
|
for i, key in enumerate(self.values.keys()):
|
|
@@ -932,7 +937,7 @@ class UpdateBuilder(QueryBuilder):
|
|
|
932
937
|
string_table = _StringTable(table_name, schema)
|
|
933
938
|
return cls(string_table, values)
|
|
934
939
|
|
|
935
|
-
def where(self, condition: Union[Condition, Template]) -> 'UpdateBuilder':
|
|
940
|
+
def where(self, condition: Union[Condition, Template, Column]) -> 'UpdateBuilder':
|
|
936
941
|
"""Add a WHERE condition (multiple calls are ANDed together)"""
|
|
937
942
|
self._conditions.append(condition)
|
|
938
943
|
self._requires_where = False
|
|
@@ -994,6 +999,9 @@ class UpdateBuilder(QueryBuilder):
|
|
|
994
999
|
for cond in self._conditions:
|
|
995
1000
|
if isinstance(cond, Template):
|
|
996
1001
|
where_parts.append(t'({cond})')
|
|
1002
|
+
elif isinstance(cond, Column):
|
|
1003
|
+
col_str = str(cond)
|
|
1004
|
+
where_parts.append(t'{col_str:literal}')
|
|
997
1005
|
else:
|
|
998
1006
|
where_parts.append(cond.to_tsql())
|
|
999
1007
|
combined_where = t_join(t' AND ', where_parts)
|
|
@@ -1057,7 +1065,7 @@ class DeleteBuilder(QueryBuilder):
|
|
|
1057
1065
|
string_table = _StringTable(table_name, schema)
|
|
1058
1066
|
return cls(string_table)
|
|
1059
1067
|
|
|
1060
|
-
def where(self, condition: Union[Condition, Template]) -> 'DeleteBuilder':
|
|
1068
|
+
def where(self, condition: Union[Condition, Template, Column]) -> 'DeleteBuilder':
|
|
1061
1069
|
"""Add a WHERE condition (multiple calls are ANDed together)"""
|
|
1062
1070
|
self._conditions.append(condition)
|
|
1063
1071
|
self._requires_where = False
|
|
@@ -1112,6 +1120,9 @@ class DeleteBuilder(QueryBuilder):
|
|
|
1112
1120
|
for cond in self._conditions:
|
|
1113
1121
|
if isinstance(cond, Template):
|
|
1114
1122
|
where_parts.append(t'({cond})')
|
|
1123
|
+
elif isinstance(cond, Column):
|
|
1124
|
+
col_str = str(cond)
|
|
1125
|
+
where_parts.append(t'{col_str:literal}')
|
|
1115
1126
|
else:
|
|
1116
1127
|
where_parts.append(cond.to_tsql())
|
|
1117
1128
|
combined_where = t_join(t' AND ', where_parts)
|
|
@@ -1210,7 +1221,7 @@ class SelectQueryBuilder(QueryBuilder):
|
|
|
1210
1221
|
self._columns = None
|
|
1211
1222
|
return self
|
|
1212
1223
|
|
|
1213
|
-
def where(self, condition: Union[Condition, Template]) -> 'SelectQueryBuilder':
|
|
1224
|
+
def where(self, condition: Union[Condition, Template, Column]) -> 'SelectQueryBuilder':
|
|
1214
1225
|
"""Add a WHERE condition (multiple calls are ANDed together)
|
|
1215
1226
|
|
|
1216
1227
|
Accepts either Condition objects from query builder or raw t-string Templates
|
|
@@ -1401,6 +1412,9 @@ class SelectQueryBuilder(QueryBuilder):
|
|
|
1401
1412
|
for cond in self._conditions:
|
|
1402
1413
|
if isinstance(cond, Template):
|
|
1403
1414
|
where_parts.append(t'({cond})')
|
|
1415
|
+
elif isinstance(cond, Column):
|
|
1416
|
+
col_str = str(cond)
|
|
1417
|
+
where_parts.append(t'{col_str:literal}')
|
|
1404
1418
|
else:
|
|
1405
1419
|
where_parts.append(cond.to_tsql())
|
|
1406
1420
|
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
|
|
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
|