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