t-sql 4.5.2__tar.gz → 4.7.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 (38) hide show
  1. t_sql-4.5.2/README.md → t_sql-4.7.0/PKG-INFO +46 -0
  2. t_sql-4.5.2/PKG-INFO → t_sql-4.7.0/README.md +36 -10
  3. {t_sql-4.5.2 → t_sql-4.7.0}/pyproject.toml +1 -1
  4. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_alembic_integration.py +142 -1
  5. t_sql-4.7.0/tests/test_deep_nesting.py +534 -0
  6. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_sqlalchemy_integration.py +168 -0
  7. t_sql-4.7.0/tests/test_string_based_builders.py +183 -0
  8. {t_sql-4.5.2 → t_sql-4.7.0}/tsql/query_builder.py +155 -26
  9. {t_sql-4.5.2 → t_sql-4.7.0}/.dockerignore +0 -0
  10. {t_sql-4.5.2 → t_sql-4.7.0}/.github/workflows/publish.yml +0 -0
  11. {t_sql-4.5.2 → t_sql-4.7.0}/.github/workflows/test.yml +0 -0
  12. {t_sql-4.5.2 → t_sql-4.7.0}/.gitignore +0 -0
  13. {t_sql-4.5.2 → t_sql-4.7.0}/Dockerfile +0 -0
  14. {t_sql-4.5.2 → t_sql-4.7.0}/LICENSE +0 -0
  15. {t_sql-4.5.2 → t_sql-4.7.0}/compose.yaml +0 -0
  16. {t_sql-4.5.2 → t_sql-4.7.0}/context7.json +0 -0
  17. {t_sql-4.5.2 → t_sql-4.7.0}/pytest.ini +0 -0
  18. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_asyncpg_integration.py +0 -0
  19. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_different_object_types.py +0 -0
  20. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_error_messages.py +0 -0
  21. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_escaped.py +0 -0
  22. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_escaped_binary_hex.py +0 -0
  23. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_helper_functions.py +0 -0
  24. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_injection_edge_cases.py +0 -0
  25. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_injection_protection_validation.py +0 -0
  26. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_injections_for_escaped.py +0 -0
  27. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_mysql_integration.py +0 -0
  28. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_parameter_names.py +0 -0
  29. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_query_builder.py +0 -0
  30. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_sqlite_integration.py +0 -0
  31. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_styles.py +0 -0
  32. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_template_in_builders.py +0 -0
  33. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_tsql.py +0 -0
  34. {t_sql-4.5.2 → t_sql-4.7.0}/tests/test_type_processor.py +0 -0
  35. {t_sql-4.5.2 → t_sql-4.7.0}/tsql/__init__.py +0 -0
  36. {t_sql-4.5.2 → t_sql-4.7.0}/tsql/row.py +0 -0
  37. {t_sql-4.5.2 → t_sql-4.7.0}/tsql/styles.py +0 -0
  38. {t_sql-4.5.2 → t_sql-4.7.0}/tsql/type_processor.py +0 -0
@@ -1,3 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: t-sql
3
+ Version: 4.7.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).
@@ -436,6 +446,42 @@ The query builder is database-agnostic - all methods are available regardless of
436
446
 
437
447
  If you use an unsupported method, your database will raise a syntax error when you execute the query.
438
448
 
449
+ ## String-Based Query Builder
450
+
451
+ t-sql also supports building queries with string table/column names instead of Table class definitions:
452
+
453
+ ```python
454
+ from tsql.query_builder import SelectQueryBuilder, InsertBuilder, UpdateBuilder, DeleteBuilder
455
+
456
+ # SELECT
457
+ user_id = 123
458
+ status = 'active'
459
+ query = SelectQueryBuilder.from_table('users', schema='public') \
460
+ .select('id', 'name', 'email') \
461
+ .where(t'id = {user_id} AND status = {status}') \
462
+ .order_by('created_at', direction='DESC') \
463
+ .limit(10)
464
+
465
+ sql, params = query.render()
466
+
467
+ # INSERT
468
+ query = InsertBuilder.into_table('users', {'name': 'Bob', 'email': 'bob@test.com'}) \
469
+ .on_conflict_do_nothing('email') \
470
+ .returning('id')
471
+
472
+ # UPDATE
473
+ cutoff_date = '2024-01-01'
474
+ query = UpdateBuilder.table('users', {'status': 'inactive'}) \
475
+ .where(t'last_login < {cutoff_date}')
476
+
477
+ # DELETE
478
+ cutoff = '2023-01-01'
479
+ query = DeleteBuilder.from_table('users') \
480
+ .where(t'created_at < {cutoff}')
481
+ ```
482
+
483
+ String identifiers are validated using the same `:literal` format spec as the core library, providing the same SQL injection protection.
484
+
439
485
  ## Mixing Query Builder with T-Strings
440
486
 
441
487
  You can combine the query builder with raw t-strings for complex logic:
@@ -1,13 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: t-sql
3
- Version: 4.5.2
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).
@@ -446,6 +436,42 @@ The query builder is database-agnostic - all methods are available regardless of
446
436
 
447
437
  If you use an unsupported method, your database will raise a syntax error when you execute the query.
448
438
 
439
+ ## String-Based Query Builder
440
+
441
+ t-sql also supports building queries with string table/column names instead of Table class definitions:
442
+
443
+ ```python
444
+ from tsql.query_builder import SelectQueryBuilder, InsertBuilder, UpdateBuilder, DeleteBuilder
445
+
446
+ # SELECT
447
+ user_id = 123
448
+ status = 'active'
449
+ query = SelectQueryBuilder.from_table('users', schema='public') \
450
+ .select('id', 'name', 'email') \
451
+ .where(t'id = {user_id} AND status = {status}') \
452
+ .order_by('created_at', direction='DESC') \
453
+ .limit(10)
454
+
455
+ sql, params = query.render()
456
+
457
+ # INSERT
458
+ query = InsertBuilder.into_table('users', {'name': 'Bob', 'email': 'bob@test.com'}) \
459
+ .on_conflict_do_nothing('email') \
460
+ .returning('id')
461
+
462
+ # UPDATE
463
+ cutoff_date = '2024-01-01'
464
+ query = UpdateBuilder.table('users', {'status': 'inactive'}) \
465
+ .where(t'last_login < {cutoff_date}')
466
+
467
+ # DELETE
468
+ cutoff = '2023-01-01'
469
+ query = DeleteBuilder.from_table('users') \
470
+ .where(t'created_at < {cutoff}')
471
+ ```
472
+
473
+ String identifiers are validated using the same `:literal` format spec as the core library, providing the same SQL injection protection.
474
+
449
475
  ## Mixing Query Builder with T-Strings
450
476
 
451
477
  You can combine the query builder with raw t-strings for complex logic:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "t-sql"
7
- version = "4.5.2"
7
+ version = "4.7.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"
@@ -465,4 +465,145 @@ def test_alembic_detects_table_comment(temp_alembic_env):
465
465
  assert len(add_table_ops) == 1
466
466
 
467
467
  table = add_table_ops[0][1]
468
- assert table.comment == 'Application configuration'
468
+ assert table.comment == 'Application configuration'
469
+
470
+
471
+ def test_alembic_detects_indexes(temp_alembic_env):
472
+ """Test that Alembic autogenerate detects indexes from indexes attribute"""
473
+ from sqlalchemy import Index
474
+
475
+ temp_dir, alembic_ini = temp_alembic_env
476
+ metadata = MetaData()
477
+ engine = create_engine("sqlite:///:memory:")
478
+
479
+ class Users(Table, table_name='users', metadata=metadata):
480
+ id = Column(String, primary_key=True)
481
+ email = Column(String, nullable=False)
482
+ username = Column(String)
483
+
484
+ indexes = [
485
+ Index('ix_users_email', 'email'),
486
+ Index('ix_users_username', 'username')
487
+ ]
488
+
489
+ cfg = Config(str(alembic_ini))
490
+ cfg.attributes['target_metadata'] = metadata
491
+ cfg.attributes['connection'] = engine
492
+
493
+ with engine.begin() as connection:
494
+ mc = MigrationContext.configure(connection)
495
+ diff = compare_metadata(mc, metadata)
496
+
497
+ # Should detect the new table
498
+ add_table_ops = [op for op in diff if op[0] == 'add_table']
499
+ assert len(add_table_ops) == 1
500
+
501
+ table = add_table_ops[0][1]
502
+ assert table.name == 'users'
503
+
504
+ # Alembic detects indexes as separate add_index operations
505
+ add_index_ops = [op for op in diff if op[0] == 'add_index']
506
+ assert len(add_index_ops) == 2
507
+
508
+ idx_names = {op[1].name for op in add_index_ops}
509
+ assert idx_names == {'ix_users_email', 'ix_users_username'}
510
+
511
+
512
+ def test_alembic_detects_gin_indexes(temp_alembic_env):
513
+ """Test that Alembic autogenerate detects GIN indexes with PostgreSQL options"""
514
+ from sqlalchemy import Index
515
+
516
+ temp_dir, alembic_ini = temp_alembic_env
517
+ metadata = MetaData()
518
+ engine = create_engine("sqlite:///:memory:")
519
+
520
+ class Documents(Table, table_name='documents', metadata=metadata):
521
+ id = Column(String, primary_key=True)
522
+ title = Column(String)
523
+ content = Column(String)
524
+
525
+ indexes = [
526
+ Index('ix_documents_title_gin', 'title',
527
+ postgresql_using='gin',
528
+ postgresql_ops={'title': 'gin_trgm_ops'}),
529
+ Index('ix_documents_content_gin', 'content',
530
+ postgresql_using='gin',
531
+ postgresql_ops={'content': 'gin_trgm_ops'})
532
+ ]
533
+
534
+ cfg = Config(str(alembic_ini))
535
+ cfg.attributes['target_metadata'] = metadata
536
+ cfg.attributes['connection'] = engine
537
+
538
+ with engine.begin() as connection:
539
+ mc = MigrationContext.configure(connection)
540
+ diff = compare_metadata(mc, metadata)
541
+
542
+ add_table_ops = [op for op in diff if op[0] == 'add_table']
543
+ assert len(add_table_ops) == 1
544
+
545
+ # Alembic detects indexes as separate add_index operations
546
+ add_index_ops = [op for op in diff if op[0] == 'add_index']
547
+ assert len(add_index_ops) == 2
548
+
549
+ idx_names = {op[1].name for op in add_index_ops}
550
+ assert 'ix_documents_title_gin' in idx_names
551
+ assert 'ix_documents_content_gin' in idx_names
552
+
553
+ # Verify PostgreSQL-specific options are preserved
554
+ for op in add_index_ops:
555
+ idx = op[1]
556
+ if idx.name == 'ix_documents_title_gin':
557
+ assert idx.dialect_options['postgresql']['using'] == 'gin'
558
+ assert idx.dialect_options['postgresql']['ops'] == {'title': 'gin_trgm_ops'}
559
+ elif idx.name == 'ix_documents_content_gin':
560
+ assert idx.dialect_options['postgresql']['using'] == 'gin'
561
+ assert idx.dialect_options['postgresql']['ops'] == {'content': 'gin_trgm_ops'}
562
+
563
+
564
+ def test_alembic_detects_indexes_and_constraints(temp_alembic_env):
565
+ """Test that Alembic autogenerate detects both indexes and constraints together"""
566
+ from sqlalchemy import Index, UniqueConstraint
567
+
568
+ temp_dir, alembic_ini = temp_alembic_env
569
+ metadata = MetaData()
570
+ engine = create_engine("sqlite:///:memory:")
571
+
572
+ class Products(Table, table_name='products', metadata=metadata):
573
+ id = Column(String, primary_key=True)
574
+ sku = Column(String, nullable=False)
575
+ name = Column(String)
576
+ category = Column(String)
577
+
578
+ constraints = [
579
+ UniqueConstraint('sku', name='uq_products_sku')
580
+ ]
581
+
582
+ indexes = [
583
+ Index('ix_products_category', 'category'),
584
+ Index('ix_products_name', 'name')
585
+ ]
586
+
587
+ cfg = Config(str(alembic_ini))
588
+ cfg.attributes['target_metadata'] = metadata
589
+ cfg.attributes['connection'] = engine
590
+
591
+ with engine.begin() as connection:
592
+ mc = MigrationContext.configure(connection)
593
+ diff = compare_metadata(mc, metadata)
594
+
595
+ add_table_ops = [op for op in diff if op[0] == 'add_table']
596
+ assert len(add_table_ops) == 1
597
+
598
+ table = add_table_ops[0][1]
599
+
600
+ # Verify constraint is in the table definition
601
+ unique_constraints = [c for c in table.constraints if isinstance(c, UniqueConstraint)]
602
+ assert len(unique_constraints) == 1
603
+ assert unique_constraints[0].name == 'uq_products_sku'
604
+
605
+ # Verify indexes are detected as separate operations
606
+ add_index_ops = [op for op in diff if op[0] == 'add_index']
607
+ assert len(add_index_ops) == 2
608
+ idx_names = {op[1].name for op in add_index_ops}
609
+ assert idx_names == {'ix_products_category', 'ix_products_name'}