t-sql 2.1.1__tar.gz → 2.1.3__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-2.1.1 → t_sql-2.1.3}/PKG-INFO +38 -8
  2. {t_sql-2.1.1 → t_sql-2.1.3}/README.md +37 -7
  3. {t_sql-2.1.1 → t_sql-2.1.3}/pyproject.toml +1 -1
  4. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_query_builder.py +26 -3
  5. {t_sql-2.1.1 → t_sql-2.1.3}/tsql/__init__.py +56 -2
  6. {t_sql-2.1.1 → t_sql-2.1.3}/tsql/query_builder.py +75 -26
  7. t_sql-2.1.1/.beads/tsql.db +0 -0
  8. t_sql-2.1.1/.idea/.gitignore +0 -8
  9. t_sql-2.1.1/.idea/inspectionProfiles/Project_Default.xml +0 -24
  10. t_sql-2.1.1/.idea/inspectionProfiles/profiles_settings.xml +0 -6
  11. t_sql-2.1.1/.idea/misc.xml +0 -11
  12. t_sql-2.1.1/.idea/tsql.iml +0 -10
  13. t_sql-2.1.1/.idea/vcs.xml +0 -6
  14. t_sql-2.1.1/.idea/workspace.xml +0 -124
  15. {t_sql-2.1.1 → t_sql-2.1.3}/.dockerignore +0 -0
  16. {t_sql-2.1.1 → t_sql-2.1.3}/.github/workflows/publish.yml +0 -0
  17. {t_sql-2.1.1 → t_sql-2.1.3}/.github/workflows/test.yml +0 -0
  18. {t_sql-2.1.1 → t_sql-2.1.3}/.gitignore +0 -0
  19. {t_sql-2.1.1 → t_sql-2.1.3}/Dockerfile +0 -0
  20. {t_sql-2.1.1 → t_sql-2.1.3}/LICENSE +0 -0
  21. {t_sql-2.1.1 → t_sql-2.1.3}/compose.yaml +0 -0
  22. {t_sql-2.1.1 → t_sql-2.1.3}/context7.json +0 -0
  23. {t_sql-2.1.1 → t_sql-2.1.3}/pytest.ini +0 -0
  24. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_alembic_integration.py +0 -0
  25. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_asyncpg_integration.py +0 -0
  26. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_different_object_types.py +0 -0
  27. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_escaped.py +0 -0
  28. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_escaped_binary_hex.py +0 -0
  29. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_helper_functions.py +0 -0
  30. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_injection_edge_cases.py +0 -0
  31. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_injection_protection_validation.py +0 -0
  32. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_injections_for_escaped.py +0 -0
  33. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_mysql_integration.py +0 -0
  34. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_sqlalchemy_integration.py +0 -0
  35. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_sqlite_integration.py +0 -0
  36. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_styles.py +0 -0
  37. {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_tsql.py +0 -0
  38. {t_sql-2.1.1 → t_sql-2.1.3}/tsql/styles.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t-sql
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
5
5
  Project-URL: Homepage, https://github.com/nhumrich/t-sql
6
6
  License-File: LICENSE
@@ -515,17 +515,47 @@ class Users(Table, table_name='user_accounts', schema='public'):
515
515
  name: Column
516
516
  ```
517
517
 
518
- # Note on Usage
518
+ # Rendering Queries
519
519
 
520
- This library should ideally be used in middleware or library code right before making a query. It can enforce the use of t-strings and prevent raw strings:
520
+ All query types (t-strings, TSQL objects, and QueryBuilder objects) can be rendered using `tsql.render()`:
521
521
 
522
522
  ```python
523
- from string.templatelib import Template
524
523
  import tsql
524
+ from tsql.query_builder import Table, Column
525
+
526
+ class Users(Table):
527
+ id: Column
528
+ name: Column
529
+
530
+ # All of these work with tsql.render():
531
+ sql, params = tsql.render(t"SELECT * FROM users WHERE id = {user_id}")
532
+ sql, params = tsql.render(Users.select().where(Users.id == user_id))
533
+ sql, params = tsql.render(tsql.select('users', user_id))
534
+
535
+ # Or call .render() directly on TSQL/QueryBuilder objects:
536
+ query = Users.select().where(Users.age > 18)
537
+ sql, params = query.render()
538
+ ```
539
+
540
+ # Type Safety & Preventing SQL Injection
541
+
542
+ This library should ideally be used in middleware or library code to enforce safe query construction. Use the `TSQLQuery` type to prevent raw strings:
525
543
 
526
- def execute_sql_query(query):
527
- if not isinstance(query, Template):
528
- raise TypeError('Cannot make a query without using t-strings')
544
+ ```python
545
+ from tsql import TSQLQuery, render
546
+
547
+ def execute_sql_query(query: TSQLQuery):
548
+ """Only accepts safe, parameterized queries"""
549
+ sql, params = render(query)
550
+ return sql_engine.execute(sql, params)
529
551
 
530
- return sql_engine.execute(*tsql.render(query))
552
+ # Type checker allows these:
553
+ execute_sql_query(t"SELECT * FROM users WHERE id = {user_id}") # ✓
554
+ execute_sql_query(Users.select()) # ✓
555
+ execute_sql_query(tsql.select('users')) # ✓
556
+
557
+ # Type checker rejects raw strings:
558
+ execute_sql_query("SELECT * FROM users") # ✗ Type error!
531
559
  ```
560
+
561
+ The `TSQLQuery` type is a union of `TSQL`, `Template` (t-strings), and `QueryBuilder`, ensuring all queries are safe from SQL injection.
@@ -505,17 +505,47 @@ class Users(Table, table_name='user_accounts', schema='public'):
505
505
  name: Column
506
506
  ```
507
507
 
508
- # Note on Usage
508
+ # Rendering Queries
509
509
 
510
- This library should ideally be used in middleware or library code right before making a query. It can enforce the use of t-strings and prevent raw strings:
510
+ All query types (t-strings, TSQL objects, and QueryBuilder objects) can be rendered using `tsql.render()`:
511
511
 
512
512
  ```python
513
- from string.templatelib import Template
514
513
  import tsql
514
+ from tsql.query_builder import Table, Column
515
+
516
+ class Users(Table):
517
+ id: Column
518
+ name: Column
519
+
520
+ # All of these work with tsql.render():
521
+ sql, params = tsql.render(t"SELECT * FROM users WHERE id = {user_id}")
522
+ sql, params = tsql.render(Users.select().where(Users.id == user_id))
523
+ sql, params = tsql.render(tsql.select('users', user_id))
524
+
525
+ # Or call .render() directly on TSQL/QueryBuilder objects:
526
+ query = Users.select().where(Users.age > 18)
527
+ sql, params = query.render()
528
+ ```
529
+
530
+ # Type Safety & Preventing SQL Injection
531
+
532
+ This library should ideally be used in middleware or library code to enforce safe query construction. Use the `TSQLQuery` type to prevent raw strings:
515
533
 
516
- def execute_sql_query(query):
517
- if not isinstance(query, Template):
518
- raise TypeError('Cannot make a query without using t-strings')
534
+ ```python
535
+ from tsql import TSQLQuery, render
536
+
537
+ def execute_sql_query(query: TSQLQuery):
538
+ """Only accepts safe, parameterized queries"""
539
+ sql, params = render(query)
540
+ return sql_engine.execute(sql, params)
519
541
 
520
- return sql_engine.execute(*tsql.render(query))
542
+ # Type checker allows these:
543
+ execute_sql_query(t"SELECT * FROM users WHERE id = {user_id}") # ✓
544
+ execute_sql_query(Users.select()) # ✓
545
+ execute_sql_query(tsql.select('users')) # ✓
546
+
547
+ # Type checker rejects raw strings:
548
+ execute_sql_query("SELECT * FROM users") # ✗ Type error!
521
549
  ```
550
+
551
+ The `TSQLQuery` type is a union of `TSQL`, `Template` (t-strings), and `QueryBuilder`, ensuring all queries are safe from SQL injection.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "t-sql"
7
- version = "2.1.1"
7
+ version = "2.1.3"
8
8
  description = "Safe SQL. SQL queries for python t-strings (PEP 750)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -31,6 +31,29 @@ def test_table_creation():
31
31
  assert Users.username.column_name == 'username'
32
32
 
33
33
 
34
+ def test_column_name_remapping():
35
+ """Test that Column(column_name=...) allows remapping Python attribute to DB column name"""
36
+ class MyTable(Table, table_name='my_table'):
37
+ id: Column
38
+ system_var: Column = Column(column_name='systemvar')
39
+
40
+ # Verify that the Python attribute name is 'system_var'
41
+ assert isinstance(MyTable.system_var, Column)
42
+ assert MyTable.system_var.table_name == 'my_table'
43
+
44
+ # But the DB column name is 'systemvar'
45
+ assert MyTable.system_var.column_name == 'systemvar'
46
+
47
+ # Verify it works in queries
48
+ query = MyTable.select(MyTable.system_var).where(MyTable.system_var == 'test')
49
+ sql, params = query.render()
50
+
51
+ # The SQL should use 'systemvar', not 'system_var'
52
+ assert 'my_table.systemvar' in sql
53
+ assert 'system_var' not in sql
54
+ assert params == ['test']
55
+
56
+
34
57
  def test_column_equality():
35
58
  """Test Column equality operator"""
36
59
  condition = Users.id == 5
@@ -256,7 +279,7 @@ def test_column_to_column_comparison():
256
279
 
257
280
 
258
281
  def test_to_tsql_returns_tsql_object():
259
- """Test that QueryBuilder.to_tsql() returns a TSQL object"""
282
+ """Test that SelectQueryBuilder.to_tsql() returns a TSQL object"""
260
283
  query = Users.select(Users.id).where(Users.id > 5)
261
284
  tsql_obj = query.to_tsql()
262
285
 
@@ -913,7 +936,7 @@ def test_where_with_not_ilike():
913
936
 
914
937
 
915
938
  def test_in_with_subquery():
916
- """Test IN with a subquery (QueryBuilder)"""
939
+ """Test IN with a subquery (SelectQueryBuilder)"""
917
940
  subquery = Users.select(Users.id).where(Users.username.like('%admin%'))
918
941
  query = Posts.select().where(Posts.user_id.in_(subquery))
919
942
  sql, params = query.render()
@@ -924,7 +947,7 @@ def test_in_with_subquery():
924
947
 
925
948
 
926
949
  def test_not_in_with_subquery():
927
- """Test NOT IN with a subquery (QueryBuilder)"""
950
+ """Test NOT IN with a subquery (SelectQueryBuilder)"""
928
951
  subquery = Users.select(Users.id).where(Users.username.like('%banned%'))
929
952
  query = Posts.select().where(Posts.user_id.not_in(subquery))
930
953
  sql, params = query.render()
@@ -1,10 +1,13 @@
1
1
  import re
2
2
  import string
3
- from typing import NamedTuple, Tuple, Any, List, Dict, Iterable
3
+ from typing import NamedTuple, Tuple, Any, List, Dict, Iterable, Union, TYPE_CHECKING
4
4
  from string.templatelib import Template, Interpolation
5
5
 
6
6
  from tsql.styles import ParamStyle, QMARK
7
7
 
8
+ if TYPE_CHECKING:
9
+ from tsql.query_builder import QueryBuilder
10
+
8
11
  default_style = QMARK
9
12
 
10
13
  def set_style(style: type[ParamStyle]):
@@ -213,7 +216,57 @@ def as_set(value_dict: dict[str, Any]):
213
216
  return tsql_obj
214
217
 
215
218
 
216
- def render(query: Template|TSQL, style=None) -> RenderedQuery:
219
+ # Type alias for safe, parameterized queries
220
+ TSQLQuery = Union[TSQL, Template, 'QueryBuilder']
221
+ """Type alias representing safe, parameterized SQL queries.
222
+
223
+ This type includes all valid ways to construct safe queries in tsql:
224
+ - TSQL: Rendered t-string queries
225
+ - Template: Raw t-string templates (PEP 750)
226
+ - QueryBuilder: Type-safe query builder objects
227
+
228
+ Use this type to ensure functions only accept safe, parameterized queries
229
+ and never raw strings that could be vulnerable to SQL injection.
230
+
231
+ Examples:
232
+ Accept any safe query type::
233
+
234
+ def execute_query(query: TSQLQuery) -> None:
235
+ sql, params = render(query)
236
+ cursor.execute(sql, params)
237
+
238
+ Type checking prevents unsafe usage::
239
+
240
+ execute_query(t"SELECT * FROM users WHERE id = {user_id}") # OK
241
+ execute_query(select("users")) # OK
242
+ execute_query(User.select().where(User.id == 1)) # OK
243
+ execute_query("SELECT * FROM users") # Type error!
244
+ """
245
+
246
+
247
+ def render(query: TSQLQuery, style=None) -> RenderedQuery:
248
+ """Render a safe query to SQL and parameters.
249
+
250
+ Args:
251
+ query: A TSQLQuery (TSQL, Template, or QueryBuilder)
252
+ style: Optional parameter style (defaults to global default_style)
253
+
254
+ Returns:
255
+ RenderedQuery with sql string and parameter values
256
+
257
+ Raises:
258
+ TypeError: If query is a raw string (use t-strings instead)
259
+ """
260
+ # Handle QueryBuilder (duck typing to avoid circular import)
261
+ if hasattr(query, 'build') and callable(query.build):
262
+ query = query.build()
263
+
264
+ if isinstance(query, str):
265
+ raise TypeError(
266
+ "Cannot render raw strings - they are vulnerable to SQL injection. "
267
+ "Use t-strings instead: t'SELECT * FROM users WHERE id = {user_id}'"
268
+ )
269
+
217
270
  if not isinstance(query, TSQL):
218
271
  query = TSQL(query)
219
272
 
@@ -305,6 +358,7 @@ def delete(table: str, id: str | int) -> TSQL:
305
358
 
306
359
  __all__ = [
307
360
  'TSQL',
361
+ 'TSQLQuery',
308
362
  'render',
309
363
  't_join',
310
364
  'select',
@@ -1,6 +1,7 @@
1
1
  from typing import Any, List, Optional, Union, ClassVar
2
2
  from string.templatelib import Template
3
3
  from datetime import datetime
4
+ from abc import ABC, abstractmethod
4
5
 
5
6
  from tsql import TSQL, t_join
6
7
 
@@ -18,7 +19,7 @@ except ImportError:
18
19
  class Column:
19
20
  """Represents a bound column (table + column name) for building queries"""
20
21
 
21
- def __init__(self, table_name: str, column_name: str, python_type: type = None, alias: str = None, schema: str = None):
22
+ def __init__(self, table_name: str = None, column_name: str = None, python_type: type = None, alias: str = None, schema: str = None):
22
23
  self.table_name = table_name
23
24
  self.column_name = column_name
24
25
  self.python_type = python_type
@@ -75,21 +76,21 @@ class Column:
75
76
  def __ge__(self, other) -> 'Condition':
76
77
  return Condition(self, '>=', other)
77
78
 
78
- def in_(self, values: Union[list, tuple, 'Column', Template, 'QueryBuilder']) -> 'Condition':
79
+ def in_(self, values: Union[list, tuple, 'Column', Template, 'SelectQueryBuilder']) -> 'Condition':
79
80
  """Create an IN condition
80
81
 
81
82
  Args:
82
- values: List/tuple of values, a Column, a Template (t-string), or a QueryBuilder for subqueries
83
+ values: List/tuple of values, a Column, a Template (t-string), or a SelectQueryBuilder for subqueries
83
84
  """
84
85
  if isinstance(values, (list, tuple)):
85
86
  return Condition(self, 'IN', tuple(values))
86
87
  return Condition(self, 'IN', values)
87
88
 
88
- def not_in(self, values: Union[list, tuple, 'Column', Template, 'QueryBuilder']) -> 'Condition':
89
+ def not_in(self, values: Union[list, tuple, 'Column', Template, 'SelectQueryBuilder']) -> 'Condition':
89
90
  """Create a NOT IN condition
90
91
 
91
92
  Args:
92
- values: List/tuple of values, a Column, a Template (t-string), or a QueryBuilder for subqueries
93
+ values: List/tuple of values, a Column, a Template (t-string), or a SelectQueryBuilder for subqueries
93
94
  """
94
95
  if isinstance(values, (list, tuple)):
95
96
  return Condition(self, 'NOT IN', tuple(values))
@@ -186,7 +187,7 @@ class Table:
186
187
  'value': getattr(cls, field_name, None)
187
188
  }
188
189
 
189
- # Then, check for Ellipsis (...) assignments and SA Columns
190
+ # Then, check for Ellipsis (...) assignments, SA Columns, and Column instances
190
191
  for field_name in dir(cls):
191
192
  if field_name.startswith('_'):
192
193
  continue
@@ -206,6 +207,13 @@ class Table:
206
207
  'type': None,
207
208
  'value': field_value
208
209
  }
210
+ # Check for Column instances (for column_name remapping)
211
+ elif isinstance(field_value, Column):
212
+ if field_name not in all_fields:
213
+ all_fields[field_name] = {
214
+ 'type': None,
215
+ 'value': field_value
216
+ }
209
217
 
210
218
  # Process all fields
211
219
  for field_name, field_info in all_fields.items():
@@ -225,6 +233,23 @@ class Table:
225
233
  setattr(cls, field_name, ColumnDescriptor(field_name, field_type))
226
234
  continue
227
235
 
236
+ # Check if it's a Column instance (for column_name remapping)
237
+ if isinstance(field_value, Column):
238
+ # Extract the column_name from the Column instance
239
+ db_column_name = field_value.column_name
240
+ if db_column_name is None:
241
+ # No column_name specified, use field_name
242
+ db_column_name = field_name
243
+
244
+ # Create query builder ColumnDescriptor with the DB column name
245
+ setattr(cls, field_name, ColumnDescriptor(db_column_name, field_type))
246
+
247
+ # Create SQLAlchemy column if metadata provided
248
+ if metadata is not None and HAS_SQLALCHEMY:
249
+ sa_type = PYTHON_TO_SA.get(field_type, String)()
250
+ sa_columns.append(SAColumn(db_column_name, sa_type))
251
+ continue
252
+
228
253
  # Check if it's an Ellipsis (...) declaration
229
254
  if field_value is ...:
230
255
  # Create query builder ColumnDescriptor
@@ -249,9 +274,9 @@ class Table:
249
274
  cls._sa_table = SATable(cls.table_name, metadata, *sa_columns, schema=schema)
250
275
 
251
276
  @classmethod
252
- def select(cls, *columns: Union['Column', Template]) -> 'QueryBuilder':
277
+ def select(cls, *columns: Union['Column', Template]) -> 'SelectQueryBuilder':
253
278
  """Start building a SELECT query"""
254
- builder = QueryBuilder(cls)
279
+ builder = SelectQueryBuilder(cls)
255
280
  if columns:
256
281
  builder.select(*columns)
257
282
  return builder
@@ -390,7 +415,31 @@ class Join:
390
415
  return t'{join_type:unsafe} JOIN {table_name:literal} ON {condition_tsql}'
391
416
 
392
417
 
393
- class InsertBuilder:
418
+ class QueryBuilder(ABC):
419
+ """Abstract base class for all query builders.
420
+
421
+ All query builders (SELECT, INSERT, UPDATE, DELETE) implement this interface,
422
+ allowing middleware and query handlers to accept any builder type.
423
+ """
424
+
425
+ @abstractmethod
426
+ def to_tsql(self) -> TSQL:
427
+ """Build and return a TSQL object representing the query."""
428
+ ...
429
+
430
+ def render(self, style=None) -> tuple[str, list]:
431
+ """Convenience method to render the query directly.
432
+
433
+ Args:
434
+ style: Optional parameter style (e.g., QMARK, NUMERIC, etc.)
435
+
436
+ Returns:
437
+ Tuple of (sql_string, parameters)
438
+ """
439
+ return self.to_tsql().render(style)
440
+
441
+
442
+ class InsertBuilder(QueryBuilder):
394
443
  """Fluent interface for building INSERT queries"""
395
444
 
396
445
  def __init__(self, base_table: 'Table', values: dict[str, Any]):
@@ -572,7 +621,7 @@ class InsertBuilder:
572
621
  return f"InsertBuilder(<error rendering: {e}>)"
573
622
 
574
623
 
575
- class UpdateBuilder:
624
+ class UpdateBuilder(QueryBuilder):
576
625
  """Fluent interface for building UPDATE queries"""
577
626
 
578
627
  def __init__(self, base_table: 'Table', values: dict[str, Any]):
@@ -663,7 +712,7 @@ class UpdateBuilder:
663
712
  return f"UpdateBuilder(<error rendering: {e}>)"
664
713
 
665
714
 
666
- class DeleteBuilder:
715
+ class DeleteBuilder(QueryBuilder):
667
716
  """Fluent interface for building DELETE queries"""
668
717
 
669
718
  def __init__(self, base_table: 'Table'):
@@ -731,8 +780,8 @@ class DeleteBuilder:
731
780
  return f"DeleteBuilder(<error rendering: {e}>)"
732
781
 
733
782
 
734
- class QueryBuilder:
735
- """Fluent interface for building SQL queries"""
783
+ class SelectQueryBuilder(QueryBuilder):
784
+ """Fluent interface for building SQL SELECT queries"""
736
785
 
737
786
  def __init__(self, base_table: 'Table'):
738
787
  self.base_table = base_table
@@ -745,7 +794,7 @@ class QueryBuilder:
745
794
  self._limit_value: Optional[int] = None
746
795
  self._offset_value: Optional[int] = None
747
796
 
748
- def select(self, *columns: Union[Column, Template]) -> 'QueryBuilder':
797
+ def select(self, *columns: Union[Column, Template]) -> 'SelectQueryBuilder':
749
798
  """Specify columns to select
750
799
 
751
800
  Args:
@@ -764,7 +813,7 @@ class QueryBuilder:
764
813
  self._columns = list(columns) if columns else None
765
814
  return self
766
815
 
767
- def where(self, condition: Union[Condition, Template]) -> 'QueryBuilder':
816
+ def where(self, condition: Union[Condition, Template]) -> 'SelectQueryBuilder':
768
817
  """Add a WHERE condition (multiple calls are ANDed together)
769
818
 
770
819
  Accepts either Condition objects from query builder or raw t-string Templates
@@ -772,20 +821,20 @@ class QueryBuilder:
772
821
  self._conditions.append(condition)
773
822
  return self
774
823
 
775
- def join(self, table: 'Table', on: Condition, join_type: str = 'INNER') -> 'QueryBuilder':
824
+ def join(self, table: 'Table', on: Condition, join_type: str = 'INNER') -> 'SelectQueryBuilder':
776
825
  """Add a JOIN clause"""
777
826
  self._joins.append(Join(table, on, join_type))
778
827
  return self
779
828
 
780
- def left_join(self, table: 'Table', on: Condition) -> 'QueryBuilder':
829
+ def left_join(self, table: 'Table', on: Condition) -> 'SelectQueryBuilder':
781
830
  """Add a LEFT JOIN clause"""
782
831
  return self.join(table, on, 'LEFT')
783
832
 
784
- def right_join(self, table: 'Table', on: Condition) -> 'QueryBuilder':
833
+ def right_join(self, table: 'Table', on: Condition) -> 'SelectQueryBuilder':
785
834
  """Add a RIGHT JOIN clause"""
786
835
  return self.join(table, on, 'RIGHT')
787
836
 
788
- def order_by(self, *columns: Union[Column, tuple[Column, str]]) -> 'QueryBuilder':
837
+ def order_by(self, *columns: Union[Column, tuple[Column, str]]) -> 'SelectQueryBuilder':
789
838
  """Add ORDER BY clause. Pass (column, 'DESC') for descending"""
790
839
  for col in columns:
791
840
  if isinstance(col, tuple):
@@ -794,12 +843,12 @@ class QueryBuilder:
794
843
  self._order_by_columns.append((col, 'ASC'))
795
844
  return self
796
845
 
797
- def group_by(self, *columns: Column) -> 'QueryBuilder':
846
+ def group_by(self, *columns: Column) -> 'SelectQueryBuilder':
798
847
  """Add GROUP BY clause"""
799
848
  self._group_by_columns.extend(columns)
800
849
  return self
801
850
 
802
- def having(self, condition: Union[Condition, Template]) -> 'QueryBuilder':
851
+ def having(self, condition: Union[Condition, Template]) -> 'SelectQueryBuilder':
803
852
  """Add HAVING condition (multiple calls are ANDed together)
804
853
 
805
854
  Accepts either Condition objects from query builder or raw t-string Templates
@@ -807,12 +856,12 @@ class QueryBuilder:
807
856
  self._having_conditions.append(condition)
808
857
  return self
809
858
 
810
- def limit(self, n: int) -> 'QueryBuilder':
859
+ def limit(self, n: int) -> 'SelectQueryBuilder':
811
860
  """Add LIMIT clause"""
812
861
  self._limit_value = n
813
862
  return self
814
863
 
815
- def offset(self, n: int) -> 'QueryBuilder':
864
+ def offset(self, n: int) -> 'SelectQueryBuilder':
816
865
  """Add OFFSET clause"""
817
866
  self._offset_value = n
818
867
  return self
@@ -894,10 +943,10 @@ class QueryBuilder:
894
943
  try:
895
944
  query, params = self.to_tsql().render()
896
945
  if params:
897
- return f"QueryBuilder(\n SQL: {query}\n Params: {params}\n)"
898
- return f"QueryBuilder({query})"
946
+ return f"SelectQueryBuilder(\n SQL: {query}\n Params: {params}\n)"
947
+ return f"SelectQueryBuilder({query})"
899
948
  except Exception as e:
900
- return f"QueryBuilder(<error rendering: {e}>)"
949
+ return f"SelectQueryBuilder(<error rendering: {e}>)"
901
950
 
902
951
 
903
952
  # Python type to SQLAlchemy type mapping (for simple type annotations)
Binary file
@@ -1,8 +0,0 @@
1
- # Default ignored files
2
- /shelf/
3
- /workspace.xml
4
- # Editor-based HTTP Client requests
5
- /httpRequests/
6
- # Datasource local storage ignored files
7
- /dataSources/
8
- /dataSources.local.xml
@@ -1,24 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <profile version="1.0">
3
- <option name="myName" value="Project Default" />
4
- <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
- <inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
6
- <option name="ourVersions">
7
- <value>
8
- <list size="3">
9
- <item index="0" class="java.lang.String" itemvalue="3.11" />
10
- <item index="1" class="java.lang.String" itemvalue="3.12" />
11
- <item index="2" class="java.lang.String" itemvalue="3.13" />
12
- </list>
13
- </value>
14
- </option>
15
- </inspection_tool>
16
- <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
17
- <option name="ignoredIdentifiers">
18
- <list>
19
- <option value="base64.binascii" />
20
- </list>
21
- </option>
22
- </inspection_tool>
23
- </profile>
24
- </component>
@@ -1,6 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <settings>
3
- <option name="USE_PROJECT_PROFILE" value="false" />
4
- <version value="1.0" />
5
- </settings>
6
- </component>
@@ -1,11 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="Black">
4
- <option name="sdkName" value="uv (tsql)" />
5
- </component>
6
- <component name="KubernetesApiPersistence">{}</component>
7
- <component name="KubernetesApiProvider"><![CDATA[{
8
- "isMigrated": true
9
- }]]></component>
10
- <component name="ProjectRootManager" version="2" project-jdk-name="uv (tsql)" project-jdk-type="Python SDK" />
11
- </project>
@@ -1,10 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module version="4">
3
- <component name="PyDocumentationSettings">
4
- <option name="format" value="PLAIN" />
5
- <option name="myDocStringFormat" value="Plain" />
6
- </component>
7
- <component name="TestRunnerService">
8
- <option name="PROJECT_TEST_RUNNER" value="py.test" />
9
- </component>
10
- </module>
t_sql-2.1.1/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="" vcs="Git" />
5
- </component>
6
- </project>
@@ -1,124 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="AutoImportSettings">
4
- <option name="autoReloadType" value="SELECTIVE" />
5
- </component>
6
- <component name="ChangeListManager">
7
- <list default="true" id="059146b3-62bd-4ad4-890d-4356cf237b89" name="Changes" comment="">
8
- <change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
9
- <change beforePath="$PROJECT_DIR$/tests/test_mysql_integration.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_mysql_integration.py" afterDir="false" />
10
- <change beforePath="$PROJECT_DIR$/tests/test_query_builder.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_query_builder.py" afterDir="false" />
11
- <change beforePath="$PROJECT_DIR$/tests/test_sqlalchemy_integration.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_sqlalchemy_integration.py" afterDir="false" />
12
- <change beforePath="$PROJECT_DIR$/tests/test_sqlite_integration.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_sqlite_integration.py" afterDir="false" />
13
- <change beforePath="$PROJECT_DIR$/tsql/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/tsql/__init__.py" afterDir="false" />
14
- <change beforePath="$PROJECT_DIR$/tsql/query_builder.py" beforeDir="false" afterPath="$PROJECT_DIR$/tsql/query_builder.py" afterDir="false" />
15
- </list>
16
- <option name="SHOW_DIALOG" value="false" />
17
- <option name="HIGHLIGHT_CONFLICTS" value="true" />
18
- <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
19
- <option name="LAST_RESOLUTION" value="IGNORE" />
20
- </component>
21
- <component name="Git.Settings">
22
- <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
23
- </component>
24
- <component name="GitHubPullRequestSearchHistory">{
25
- &quot;lastFilter&quot;: {
26
- &quot;state&quot;: &quot;OPEN&quot;,
27
- &quot;assignee&quot;: &quot;nhumrich&quot;
28
- }
29
- }</component>
30
- <component name="GithubPullRequestsUISettings">{
31
- &quot;selectedUrlAndAccountId&quot;: {
32
- &quot;url&quot;: &quot;git@github.com:nhumrich/tsql.git&quot;,
33
- &quot;accountId&quot;: &quot;f308fc0d-c429-47fb-8e52-74bdc95408d8&quot;
34
- }
35
- }</component>
36
- <component name="ProjectColorInfo">{
37
- &quot;associatedIndex&quot;: 2
38
- }</component>
39
- <component name="ProjectId" id="33DagtaPqCq8RWQhigWGQcgOtLh" />
40
- <component name="ProjectViewState">
41
- <option name="compactDirectories" value="true" />
42
- <option name="hideEmptyMiddlePackages" value="true" />
43
- <option name="showLibraryContents" value="true" />
44
- </component>
45
- <component name="PropertiesComponent">{
46
- &quot;keyToString&quot;: {
47
- &quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
48
- &quot;Python tests.pytest in test_sqlalchemy_integration.py.executor&quot;: &quot;Debug&quot;,
49
- &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
50
- &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
51
- &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
52
- &quot;git-widget-placeholder&quot;: &quot;main&quot;,
53
- &quot;junie.onboarding.icon.badge.shown&quot;: &quot;true&quot;,
54
- &quot;last_opened_file_path&quot;: &quot;/home/nhumrich/personal/tsql&quot;,
55
- &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
56
- &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
57
- &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
58
- &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
59
- &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
60
- &quot;to.speed.mode.migration.done&quot;: &quot;true&quot;,
61
- &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
62
- }
63
- }</component>
64
- <component name="RunManager">
65
- <configuration name="pytest in test_sqlalchemy_integration.py" type="tests" factoryName="py.test" temporary="true" nameIsGenerated="true">
66
- <module name="tsql" />
67
- <option name="ENV_FILES" value="" />
68
- <option name="INTERPRETER_OPTIONS" value="" />
69
- <option name="PARENT_ENVS" value="true" />
70
- <option name="SDK_HOME" value="" />
71
- <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
72
- <option name="IS_MODULE_SDK" value="true" />
73
- <option name="ADD_CONTENT_ROOTS" value="true" />
74
- <option name="ADD_SOURCE_ROOTS" value="true" />
75
- <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
76
- <option name="_new_keywords" value="&quot;&quot;" />
77
- <option name="_new_parameters" value="&quot;&quot;" />
78
- <option name="_new_additionalArguments" value="&quot;&quot;" />
79
- <option name="_new_target" value="&quot;$PROJECT_DIR$/tests/test_sqlalchemy_integration.py&quot;" />
80
- <option name="_new_targetType" value="&quot;PATH&quot;" />
81
- <method v="2" />
82
- </configuration>
83
- <recent_temporary>
84
- <list>
85
- <item itemvalue="Python tests.pytest in test_sqlalchemy_integration.py" />
86
- </list>
87
- </recent_temporary>
88
- </component>
89
- <component name="SharedIndexes">
90
- <attachedChunks>
91
- <set>
92
- <option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-PY-252.26830.99" />
93
- <option value="bundled-python-sdk-164cda30dcd9-0af03a5fa574-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.26830.99" />
94
- </set>
95
- </attachedChunks>
96
- </component>
97
- <component name="TaskManager">
98
- <task active="true" id="Default" summary="Default task">
99
- <changelist id="059146b3-62bd-4ad4-890d-4356cf237b89" name="Changes" comment="" />
100
- <created>1758854212149</created>
101
- <option name="number" value="Default" />
102
- <option name="presentableId" value="Default" />
103
- <updated>1758854212149</updated>
104
- </task>
105
- <servers />
106
- </component>
107
- <component name="TypeScriptGeneratedFilesManager">
108
- <option name="version" value="3" />
109
- </component>
110
- <component name="Vcs.Log.Tabs.Properties">
111
- <option name="TAB_STATES">
112
- <map>
113
- <entry key="MAIN">
114
- <value>
115
- <State />
116
- </value>
117
- </entry>
118
- </map>
119
- </option>
120
- </component>
121
- <component name="com.intellij.coverage.CoverageDataManagerImpl">
122
- <SUITE FILE_PATH="coverage/tsql$pytest_in_test_sqlalchemy_integration_py.coverage" NAME="pytest in test_sqlalchemy_integration.py Coverage Results" MODIFIED="1760138305802" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
123
- </component>
124
- </project>
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