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.
Files changed (39) hide show
  1. t_sql-4.10.0/README.md → t_sql-4.11.0/PKG-INFO +18 -0
  2. t_sql-4.10.0/PKG-INFO → t_sql-4.11.0/README.md +8 -10
  3. {t_sql-4.10.0 → t_sql-4.11.0}/pyproject.toml +1 -1
  4. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_asyncpg_integration.py +32 -1
  5. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_query_builder.py +37 -0
  6. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_sqlite_integration.py +26 -0
  7. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_tsql.py +1 -1
  8. {t_sql-4.10.0 → t_sql-4.11.0}/tsql/query_builder.py +7 -2
  9. {t_sql-4.10.0 → t_sql-4.11.0}/.dockerignore +0 -0
  10. {t_sql-4.10.0 → t_sql-4.11.0}/.github/workflows/publish.yml +0 -0
  11. {t_sql-4.10.0 → t_sql-4.11.0}/.github/workflows/test.yml +0 -0
  12. {t_sql-4.10.0 → t_sql-4.11.0}/.gitignore +0 -0
  13. {t_sql-4.10.0 → t_sql-4.11.0}/Dockerfile +0 -0
  14. {t_sql-4.10.0 → t_sql-4.11.0}/LICENSE +0 -0
  15. {t_sql-4.10.0 → t_sql-4.11.0}/compose.yaml +0 -0
  16. {t_sql-4.10.0 → t_sql-4.11.0}/context7.json +0 -0
  17. {t_sql-4.10.0 → t_sql-4.11.0}/pytest.ini +0 -0
  18. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_alembic_integration.py +0 -0
  19. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_deep_nesting.py +0 -0
  20. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_different_object_types.py +0 -0
  21. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_error_messages.py +0 -0
  22. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_escaped.py +0 -0
  23. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_escaped_binary_hex.py +0 -0
  24. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_helper_functions.py +0 -0
  25. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_injection_edge_cases.py +0 -0
  26. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_injection_protection_validation.py +0 -0
  27. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_injections_for_escaped.py +0 -0
  28. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_like_patterns.py +0 -0
  29. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_mysql_integration.py +0 -0
  30. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_parameter_names.py +0 -0
  31. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_sqlalchemy_integration.py +0 -0
  32. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_string_based_builders.py +0 -0
  33. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_styles.py +0 -0
  34. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_template_in_builders.py +0 -0
  35. {t_sql-4.10.0 → t_sql-4.11.0}/tests/test_type_processor.py +0 -0
  36. {t_sql-4.10.0 → t_sql-4.11.0}/tsql/__init__.py +0 -0
  37. {t_sql-4.10.0 → t_sql-4.11.0}/tsql/row.py +0 -0
  38. {t_sql-4.10.0 → t_sql-4.11.0}/tsql/styles.py +0 -0
  39. {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()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "t-sql"
7
- version = "4.10.0"
7
+ version = "4.11.0"
8
8
  description = "Safe SQL. SQL queries for python t-strings (PEP 750)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -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): # TODO: change to appropriate error
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
- # MySQL INSERT IGNORE
763
- if self._ignore:
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