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.
- {t_sql-2.1.1 → t_sql-2.1.3}/PKG-INFO +38 -8
- {t_sql-2.1.1 → t_sql-2.1.3}/README.md +37 -7
- {t_sql-2.1.1 → t_sql-2.1.3}/pyproject.toml +1 -1
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_query_builder.py +26 -3
- {t_sql-2.1.1 → t_sql-2.1.3}/tsql/__init__.py +56 -2
- {t_sql-2.1.1 → t_sql-2.1.3}/tsql/query_builder.py +75 -26
- t_sql-2.1.1/.beads/tsql.db +0 -0
- t_sql-2.1.1/.idea/.gitignore +0 -8
- t_sql-2.1.1/.idea/inspectionProfiles/Project_Default.xml +0 -24
- t_sql-2.1.1/.idea/inspectionProfiles/profiles_settings.xml +0 -6
- t_sql-2.1.1/.idea/misc.xml +0 -11
- t_sql-2.1.1/.idea/tsql.iml +0 -10
- t_sql-2.1.1/.idea/vcs.xml +0 -6
- t_sql-2.1.1/.idea/workspace.xml +0 -124
- {t_sql-2.1.1 → t_sql-2.1.3}/.dockerignore +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/.github/workflows/publish.yml +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/.github/workflows/test.yml +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/.gitignore +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/Dockerfile +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/LICENSE +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/compose.yaml +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/context7.json +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/pytest.ini +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_alembic_integration.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_different_object_types.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_escaped.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_helper_functions.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_mysql_integration.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_sqlite_integration.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_styles.py +0 -0
- {t_sql-2.1.1 → t_sql-2.1.3}/tests/test_tsql.py +0 -0
- {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.
|
|
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
|
-
#
|
|
518
|
+
# Rendering Queries
|
|
519
519
|
|
|
520
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
508
|
+
# Rendering Queries
|
|
509
509
|
|
|
510
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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, '
|
|
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
|
|
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, '
|
|
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
|
|
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
|
|
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]) -> '
|
|
277
|
+
def select(cls, *columns: Union['Column', Template]) -> 'SelectQueryBuilder':
|
|
253
278
|
"""Start building a SELECT query"""
|
|
254
|
-
builder =
|
|
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
|
|
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]) -> '
|
|
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]) -> '
|
|
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') -> '
|
|
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) -> '
|
|
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) -> '
|
|
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]]) -> '
|
|
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) -> '
|
|
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]) -> '
|
|
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) -> '
|
|
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) -> '
|
|
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"
|
|
898
|
-
return f"
|
|
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"
|
|
949
|
+
return f"SelectQueryBuilder(<error rendering: {e}>)"
|
|
901
950
|
|
|
902
951
|
|
|
903
952
|
# Python type to SQLAlchemy type mapping (for simple type annotations)
|
t_sql-2.1.1/.beads/tsql.db
DELETED
|
Binary file
|
t_sql-2.1.1/.idea/.gitignore
DELETED
|
@@ -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>
|
t_sql-2.1.1/.idea/misc.xml
DELETED
|
@@ -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>
|
t_sql-2.1.1/.idea/tsql.iml
DELETED
|
@@ -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
t_sql-2.1.1/.idea/workspace.xml
DELETED
|
@@ -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
|
-
"lastFilter": {
|
|
26
|
-
"state": "OPEN",
|
|
27
|
-
"assignee": "nhumrich"
|
|
28
|
-
}
|
|
29
|
-
}</component>
|
|
30
|
-
<component name="GithubPullRequestsUISettings">{
|
|
31
|
-
"selectedUrlAndAccountId": {
|
|
32
|
-
"url": "git@github.com:nhumrich/tsql.git",
|
|
33
|
-
"accountId": "f308fc0d-c429-47fb-8e52-74bdc95408d8"
|
|
34
|
-
}
|
|
35
|
-
}</component>
|
|
36
|
-
<component name="ProjectColorInfo">{
|
|
37
|
-
"associatedIndex": 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
|
-
"keyToString": {
|
|
47
|
-
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
48
|
-
"Python tests.pytest in test_sqlalchemy_integration.py.executor": "Debug",
|
|
49
|
-
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
50
|
-
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
51
|
-
"RunOnceActivity.git.unshallow": "true",
|
|
52
|
-
"git-widget-placeholder": "main",
|
|
53
|
-
"junie.onboarding.icon.badge.shown": "true",
|
|
54
|
-
"last_opened_file_path": "/home/nhumrich/personal/tsql",
|
|
55
|
-
"node.js.detected.package.eslint": "true",
|
|
56
|
-
"node.js.detected.package.tslint": "true",
|
|
57
|
-
"node.js.selected.package.eslint": "(autodetect)",
|
|
58
|
-
"node.js.selected.package.tslint": "(autodetect)",
|
|
59
|
-
"nodejs_package_manager_path": "npm",
|
|
60
|
-
"to.speed.mode.migration.done": "true",
|
|
61
|
-
"vue.rearranger.settings.migration": "true"
|
|
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="""" />
|
|
77
|
-
<option name="_new_parameters" value="""" />
|
|
78
|
-
<option name="_new_additionalArguments" value="""" />
|
|
79
|
-
<option name="_new_target" value=""$PROJECT_DIR$/tests/test_sqlalchemy_integration.py"" />
|
|
80
|
-
<option name="_new_targetType" value=""PATH"" />
|
|
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
|
|
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
|