t-sql 4.0.0__tar.gz → 4.2.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.0.0 → t_sql-4.2.0}/PKG-INFO +11 -20
- {t_sql-4.0.0 → t_sql-4.2.0}/README.md +10 -19
- {t_sql-4.0.0 → t_sql-4.2.0}/pyproject.toml +4 -1
- {t_sql-4.0.0 → t_sql-4.2.0}/tsql/query_builder.py +105 -63
- {t_sql-4.0.0 → t_sql-4.2.0}/.dockerignore +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/.github/workflows/publish.yml +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/.github/workflows/test.yml +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/.gitignore +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/Dockerfile +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/LICENSE +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/compose.yaml +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/context7.json +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/pytest.ini +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_alembic_integration.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_different_object_types.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_escaped.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_helper_functions.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_mysql_integration.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_parameter_names.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_query_builder.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_sqlite_integration.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_styles.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tests/test_tsql.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tsql/__init__.py +0 -0
- {t_sql-4.0.0 → t_sql-4.2.0}/tsql/styles.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: t-sql
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
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
|
|
@@ -495,7 +495,7 @@ uv add t-sql --optional sqlalchemy
|
|
|
495
495
|
**1. Simple Column annotations** (for query builder only):
|
|
496
496
|
|
|
497
497
|
```python
|
|
498
|
-
from tsql import Table, Column
|
|
498
|
+
from tsql.query_builder import Table, Column
|
|
499
499
|
|
|
500
500
|
class Users(Table):
|
|
501
501
|
id: Column
|
|
@@ -503,38 +503,29 @@ class Users(Table):
|
|
|
503
503
|
age: Column
|
|
504
504
|
```
|
|
505
505
|
|
|
506
|
-
**2. SQLAlchemy
|
|
506
|
+
**2. SQLAlchemy with SAColumn wrapper** (recommended for type checkers):
|
|
507
507
|
|
|
508
508
|
```python
|
|
509
|
-
from sqlalchemy import MetaData,
|
|
510
|
-
from tsql.query_builder import Table
|
|
509
|
+
from sqlalchemy import MetaData, Integer, String
|
|
510
|
+
from tsql.query_builder import Table, SAColumn
|
|
511
511
|
|
|
512
512
|
metadata = MetaData()
|
|
513
513
|
|
|
514
514
|
class Users(Table, metadata=metadata):
|
|
515
|
-
id =
|
|
516
|
-
email =
|
|
517
|
-
name =
|
|
518
|
-
age =
|
|
515
|
+
id = SAColumn(Integer, primary_key=True)
|
|
516
|
+
email = SAColumn(String(255), unique=True, nullable=False)
|
|
517
|
+
name = SAColumn(String(100))
|
|
518
|
+
age = SAColumn(Integer)
|
|
519
519
|
|
|
520
520
|
# Use for alembic
|
|
521
521
|
target_metadata = metadata
|
|
522
522
|
|
|
523
|
-
# Use for queries
|
|
523
|
+
# Use for queries
|
|
524
524
|
query = Users.select().where(Users.age > 18)
|
|
525
525
|
```
|
|
526
526
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
```python
|
|
530
|
-
from sqlalchemy import Column, String, DateTime
|
|
531
|
-
from sqlalchemy.sql.functions import now
|
|
527
|
+
The `SAColumn` wrapper tells type checkers it returns a tsql `Column`, while at runtime it creates a SQLAlchemy `Column`. This gives you proper IDE completions for methods like `.is_null()`, `.like()`, etc.
|
|
532
528
|
|
|
533
|
-
class Events(Table, metadata=metadata):
|
|
534
|
-
id = Column(String, primary_key=True)
|
|
535
|
-
topic: Column # Simple annotation - becomes nullable String column
|
|
536
|
-
created_at = Column(DateTime, server_default=now())
|
|
537
|
-
```
|
|
538
529
|
|
|
539
530
|
## Schema Support
|
|
540
531
|
|
|
@@ -485,7 +485,7 @@ uv add t-sql --optional sqlalchemy
|
|
|
485
485
|
**1. Simple Column annotations** (for query builder only):
|
|
486
486
|
|
|
487
487
|
```python
|
|
488
|
-
from tsql import Table, Column
|
|
488
|
+
from tsql.query_builder import Table, Column
|
|
489
489
|
|
|
490
490
|
class Users(Table):
|
|
491
491
|
id: Column
|
|
@@ -493,38 +493,29 @@ class Users(Table):
|
|
|
493
493
|
age: Column
|
|
494
494
|
```
|
|
495
495
|
|
|
496
|
-
**2. SQLAlchemy
|
|
496
|
+
**2. SQLAlchemy with SAColumn wrapper** (recommended for type checkers):
|
|
497
497
|
|
|
498
498
|
```python
|
|
499
|
-
from sqlalchemy import MetaData,
|
|
500
|
-
from tsql.query_builder import Table
|
|
499
|
+
from sqlalchemy import MetaData, Integer, String
|
|
500
|
+
from tsql.query_builder import Table, SAColumn
|
|
501
501
|
|
|
502
502
|
metadata = MetaData()
|
|
503
503
|
|
|
504
504
|
class Users(Table, metadata=metadata):
|
|
505
|
-
id =
|
|
506
|
-
email =
|
|
507
|
-
name =
|
|
508
|
-
age =
|
|
505
|
+
id = SAColumn(Integer, primary_key=True)
|
|
506
|
+
email = SAColumn(String(255), unique=True, nullable=False)
|
|
507
|
+
name = SAColumn(String(100))
|
|
508
|
+
age = SAColumn(Integer)
|
|
509
509
|
|
|
510
510
|
# Use for alembic
|
|
511
511
|
target_metadata = metadata
|
|
512
512
|
|
|
513
|
-
# Use for queries
|
|
513
|
+
# Use for queries
|
|
514
514
|
query = Users.select().where(Users.age > 18)
|
|
515
515
|
```
|
|
516
516
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
```python
|
|
520
|
-
from sqlalchemy import Column, String, DateTime
|
|
521
|
-
from sqlalchemy.sql.functions import now
|
|
517
|
+
The `SAColumn` wrapper tells type checkers it returns a tsql `Column`, while at runtime it creates a SQLAlchemy `Column`. This gives you proper IDE completions for methods like `.is_null()`, `.like()`, etc.
|
|
522
518
|
|
|
523
|
-
class Events(Table, metadata=metadata):
|
|
524
|
-
id = Column(String, primary_key=True)
|
|
525
|
-
topic: Column # Simple annotation - becomes nullable String column
|
|
526
|
-
created_at = Column(DateTime, server_default=now())
|
|
527
|
-
```
|
|
528
519
|
|
|
529
520
|
## Schema Support
|
|
530
521
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "t-sql"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.2.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"
|
|
@@ -23,9 +23,12 @@ dev = [
|
|
|
23
23
|
"anyio>=4.9.0",
|
|
24
24
|
"asyncpg>=0.30.0",
|
|
25
25
|
"cryptography>=46.0.2",
|
|
26
|
+
"mypy>=1.18.2",
|
|
27
|
+
"pyright>=1.1.406",
|
|
26
28
|
"pytest>=8.3.5",
|
|
27
29
|
"pytest-asyncio>=0.24.0",
|
|
28
30
|
"sqlalchemy>=2.0.0",
|
|
31
|
+
"ty>=0.0.1a23",
|
|
29
32
|
]
|
|
30
33
|
|
|
31
34
|
[tool.hatch.build.targets.wheel]
|
|
@@ -38,7 +38,7 @@ class OrderByClause:
|
|
|
38
38
|
class Column:
|
|
39
39
|
"""Represents a bound column (table + column name) for building queries"""
|
|
40
40
|
|
|
41
|
-
def __init__(self, table_name: str = None, column_name: str = None, alias: str = None, schema: str = None):
|
|
41
|
+
def __init__(self, table_name: str | None = None, column_name: str | None = None, alias: str | None = None, schema: str | None = None):
|
|
42
42
|
self.table_name = table_name
|
|
43
43
|
self.column_name = column_name
|
|
44
44
|
self.alias = alias
|
|
@@ -194,13 +194,22 @@ class Table:
|
|
|
194
194
|
class Users(Table, table_name='user_accounts'):
|
|
195
195
|
id: Column
|
|
196
196
|
|
|
197
|
-
For SQLAlchemy integration, use
|
|
197
|
+
For SQLAlchemy integration, use the SAColumn wrapper for type checker compatibility:
|
|
198
198
|
|
|
199
|
-
from sqlalchemy import
|
|
199
|
+
from sqlalchemy import Integer, String
|
|
200
|
+
from tsql.query_builder import Table, SAColumn
|
|
200
201
|
|
|
201
202
|
class Users(Table, metadata=metadata, schema='public'):
|
|
202
|
-
id =
|
|
203
|
-
name =
|
|
203
|
+
id = SAColumn(Integer, primary_key=True)
|
|
204
|
+
name = SAColumn(String(100))
|
|
205
|
+
|
|
206
|
+
Alternative: Use explicit type annotations with SQLAlchemy Column:
|
|
207
|
+
|
|
208
|
+
from sqlalchemy import Column as SACol
|
|
209
|
+
|
|
210
|
+
class Users(Table, metadata=metadata):
|
|
211
|
+
id: Column = SACol(Integer, primary_key=True)
|
|
212
|
+
name: Column = SACol(String(100))
|
|
204
213
|
"""
|
|
205
214
|
table_name: ClassVar[str]
|
|
206
215
|
schema: ClassVar[Optional[str]] = None
|
|
@@ -231,7 +240,7 @@ class Table:
|
|
|
231
240
|
continue
|
|
232
241
|
field_value = getattr(cls, field_name, None)
|
|
233
242
|
|
|
234
|
-
# Check for Ellipsis syntax: id =
|
|
243
|
+
# Check for Ellipsis syntax: id = ...~
|
|
235
244
|
if field_value is ...:
|
|
236
245
|
if field_name not in all_fields:
|
|
237
246
|
all_fields[field_name] = {
|
|
@@ -267,9 +276,9 @@ class Table:
|
|
|
267
276
|
sa_col.name = field_name
|
|
268
277
|
sa_columns.append(sa_col)
|
|
269
278
|
|
|
270
|
-
# Create query builder
|
|
271
|
-
setattr(cls, field_name,
|
|
272
|
-
# Update annotation to reflect the
|
|
279
|
+
# Create query builder Column directly
|
|
280
|
+
setattr(cls, field_name, Column(cls.table_name, field_name, schema=schema))
|
|
281
|
+
# Update annotation to reflect the Column type
|
|
273
282
|
if not hasattr(cls, '__annotations__'):
|
|
274
283
|
cls.__annotations__ = {}
|
|
275
284
|
cls.__annotations__[field_name] = Column
|
|
@@ -283,9 +292,9 @@ class Table:
|
|
|
283
292
|
# No column_name specified, use field_name
|
|
284
293
|
db_column_name = field_name
|
|
285
294
|
|
|
286
|
-
# Create query builder
|
|
287
|
-
setattr(cls, field_name,
|
|
288
|
-
# Update annotation to reflect the
|
|
295
|
+
# Create query builder Column directly with the DB column name
|
|
296
|
+
setattr(cls, field_name, Column(cls.table_name, db_column_name, schema=schema))
|
|
297
|
+
# Update annotation to reflect the Column type
|
|
289
298
|
if not hasattr(cls, '__annotations__'):
|
|
290
299
|
cls.__annotations__ = {}
|
|
291
300
|
cls.__annotations__[field_name] = Column
|
|
@@ -298,9 +307,9 @@ class Table:
|
|
|
298
307
|
|
|
299
308
|
# Check if it's an Ellipsis (...) declaration
|
|
300
309
|
if field_value is ...:
|
|
301
|
-
# Create query builder
|
|
302
|
-
setattr(cls, field_name,
|
|
303
|
-
# Update annotation to reflect the
|
|
310
|
+
# Create query builder Column directly
|
|
311
|
+
setattr(cls, field_name, Column(cls.table_name, field_name, schema=schema))
|
|
312
|
+
# Update annotation to reflect the Column type
|
|
304
313
|
if not hasattr(cls, '__annotations__'):
|
|
305
314
|
cls.__annotations__ = {}
|
|
306
315
|
cls.__annotations__[field_name] = Column
|
|
@@ -311,9 +320,9 @@ class Table:
|
|
|
311
320
|
# No type annotation, Ellipsis, or SA Column - skip
|
|
312
321
|
continue
|
|
313
322
|
|
|
314
|
-
# Create query builder
|
|
315
|
-
setattr(cls, field_name,
|
|
316
|
-
# Update annotation to reflect the
|
|
323
|
+
# Create query builder Column directly for type-annotated fields
|
|
324
|
+
setattr(cls, field_name, Column(cls.table_name, field_name, schema=schema))
|
|
325
|
+
# Update annotation to reflect the Column type
|
|
317
326
|
if not hasattr(cls, '__annotations__'):
|
|
318
327
|
cls.__annotations__ = {}
|
|
319
328
|
cls.__annotations__[field_name] = Column
|
|
@@ -327,8 +336,8 @@ class Table:
|
|
|
327
336
|
if metadata is not None and HAS_SQLALCHEMY:
|
|
328
337
|
cls._sa_table = SATable(cls.table_name, metadata, *sa_columns, schema=schema)
|
|
329
338
|
|
|
330
|
-
# Add the ALL
|
|
331
|
-
cls.ALL =
|
|
339
|
+
# Add the ALL column for wildcard column selection
|
|
340
|
+
cls.ALL = Column(cls.table_name, '*', schema=schema)
|
|
332
341
|
|
|
333
342
|
@classmethod
|
|
334
343
|
def select(cls, *columns: Union['Column', Template]) -> 'SelectQueryBuilder':
|
|
@@ -380,32 +389,6 @@ class Table:
|
|
|
380
389
|
return DeleteBuilder(cls)
|
|
381
390
|
|
|
382
391
|
|
|
383
|
-
class ColumnDescriptor:
|
|
384
|
-
"""Descriptor that creates Column objects when accessed on Table classes or instances"""
|
|
385
|
-
|
|
386
|
-
def __init__(self, column_name: str):
|
|
387
|
-
self.column_name = column_name
|
|
388
|
-
|
|
389
|
-
def __set_name__(self, owner, name):
|
|
390
|
-
self.column_name = name
|
|
391
|
-
|
|
392
|
-
def __get__(self, obj, objtype=None) -> Column:
|
|
393
|
-
if objtype is None:
|
|
394
|
-
objtype = type(obj)
|
|
395
|
-
schema = getattr(objtype, 'schema', None)
|
|
396
|
-
return Column(objtype.table_name, self.column_name, schema=schema)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
class AllColumnsDescriptor:
|
|
400
|
-
"""Descriptor that creates a Column with wildcard (*) for selecting all columns from a table"""
|
|
401
|
-
|
|
402
|
-
def __get__(self, obj, objtype=None) -> Column:
|
|
403
|
-
if objtype is None:
|
|
404
|
-
objtype = type(obj)
|
|
405
|
-
schema = getattr(objtype, 'schema', None)
|
|
406
|
-
return Column(objtype.table_name, '*', schema=schema)
|
|
407
|
-
|
|
408
|
-
|
|
409
392
|
class Condition:
|
|
410
393
|
"""Represents a WHERE clause condition"""
|
|
411
394
|
|
|
@@ -469,7 +452,7 @@ class Condition:
|
|
|
469
452
|
class Join:
|
|
470
453
|
"""Represents a JOIN clause"""
|
|
471
454
|
|
|
472
|
-
def __init__(self, table: 'Table', condition: Condition, join_type: str = 'INNER'):
|
|
455
|
+
def __init__(self, table: type['Table'], condition: Condition, join_type: str = 'INNER'):
|
|
473
456
|
self.table = table
|
|
474
457
|
self.condition = condition
|
|
475
458
|
self.join_type = join_type
|
|
@@ -512,7 +495,7 @@ class QueryBuilder(ABC):
|
|
|
512
495
|
class InsertBuilder(QueryBuilder):
|
|
513
496
|
"""Fluent interface for building INSERT queries"""
|
|
514
497
|
|
|
515
|
-
def __init__(self, base_table: 'Table', values: dict[str, Any]):
|
|
498
|
+
def __init__(self, base_table: type['Table'], values: dict[str, Any]):
|
|
516
499
|
self.base_table = base_table
|
|
517
500
|
|
|
518
501
|
# Apply defaults from SQLAlchemy columns if available
|
|
@@ -580,13 +563,23 @@ class InsertBuilder(QueryBuilder):
|
|
|
580
563
|
self._update_cols = update
|
|
581
564
|
return self
|
|
582
565
|
|
|
583
|
-
def returning(self, *columns: str) -> 'InsertBuilder':
|
|
566
|
+
def returning(self, *columns: Union[str, Column]) -> 'InsertBuilder':
|
|
584
567
|
"""Add RETURNING clause (Postgres/SQLite only)
|
|
585
568
|
|
|
586
569
|
Args:
|
|
587
|
-
columns: Column names to return, or none for RETURNING *
|
|
570
|
+
columns: Column names (strings) or Column objects to return, or none for RETURNING *
|
|
588
571
|
"""
|
|
589
|
-
|
|
572
|
+
if columns:
|
|
573
|
+
# Convert Column objects to their column names
|
|
574
|
+
col_names = []
|
|
575
|
+
for col in columns:
|
|
576
|
+
if isinstance(col, Column):
|
|
577
|
+
col_names.append(col.column_name)
|
|
578
|
+
else:
|
|
579
|
+
col_names.append(col)
|
|
580
|
+
self._returning_cols = col_names
|
|
581
|
+
else:
|
|
582
|
+
self._returning_cols = ['*']
|
|
590
583
|
return self
|
|
591
584
|
|
|
592
585
|
def to_tsql(self) -> TSQL:
|
|
@@ -694,7 +687,7 @@ class InsertBuilder(QueryBuilder):
|
|
|
694
687
|
class UpdateBuilder(QueryBuilder):
|
|
695
688
|
"""Fluent interface for building UPDATE queries"""
|
|
696
689
|
|
|
697
|
-
def __init__(self, base_table: 'Table', values: dict[str, Any]):
|
|
690
|
+
def __init__(self, base_table: type['Table'], values: dict[str, Any]):
|
|
698
691
|
self.base_table = base_table
|
|
699
692
|
|
|
700
693
|
# Apply onupdate defaults from SQLAlchemy columns if available
|
|
@@ -743,13 +736,23 @@ class UpdateBuilder(QueryBuilder):
|
|
|
743
736
|
self._requires_where = False
|
|
744
737
|
return self
|
|
745
738
|
|
|
746
|
-
def returning(self, *columns: str) -> 'UpdateBuilder':
|
|
739
|
+
def returning(self, *columns: Union[str, Column]) -> 'UpdateBuilder':
|
|
747
740
|
"""Add RETURNING clause (Postgres/SQLite only)
|
|
748
741
|
|
|
749
742
|
Args:
|
|
750
|
-
columns: Column names to return, or none for RETURNING *
|
|
743
|
+
columns: Column names (strings) or Column objects to return, or none for RETURNING *
|
|
751
744
|
"""
|
|
752
|
-
|
|
745
|
+
if columns:
|
|
746
|
+
# Convert Column objects to their column names
|
|
747
|
+
col_names = []
|
|
748
|
+
for col in columns:
|
|
749
|
+
if isinstance(col, Column):
|
|
750
|
+
col_names.append(col.column_name)
|
|
751
|
+
else:
|
|
752
|
+
col_names.append(col)
|
|
753
|
+
self._returning_cols = col_names
|
|
754
|
+
else:
|
|
755
|
+
self._returning_cols = ['*']
|
|
753
756
|
return self
|
|
754
757
|
|
|
755
758
|
def to_tsql(self) -> TSQL:
|
|
@@ -807,7 +810,7 @@ class UpdateBuilder(QueryBuilder):
|
|
|
807
810
|
class DeleteBuilder(QueryBuilder):
|
|
808
811
|
"""Fluent interface for building DELETE queries"""
|
|
809
812
|
|
|
810
|
-
def __init__(self, base_table: 'Table'):
|
|
813
|
+
def __init__(self, base_table: type['Table']):
|
|
811
814
|
self.base_table = base_table
|
|
812
815
|
self._conditions: List[Union[Condition, Template]] = []
|
|
813
816
|
self._returning_cols: Optional[List[str]] = None
|
|
@@ -834,13 +837,23 @@ class DeleteBuilder(QueryBuilder):
|
|
|
834
837
|
self._requires_where = False
|
|
835
838
|
return self
|
|
836
839
|
|
|
837
|
-
def returning(self, *columns: str) -> 'DeleteBuilder':
|
|
840
|
+
def returning(self, *columns: Union[str, Column]) -> 'DeleteBuilder':
|
|
838
841
|
"""Add RETURNING clause (Postgres/SQLite only)
|
|
839
842
|
|
|
840
843
|
Args:
|
|
841
|
-
columns: Column names to return, or none for RETURNING *
|
|
844
|
+
columns: Column names (strings) or Column objects to return, or none for RETURNING *
|
|
842
845
|
"""
|
|
843
|
-
|
|
846
|
+
if columns:
|
|
847
|
+
# Convert Column objects to their column names
|
|
848
|
+
col_names = []
|
|
849
|
+
for col in columns:
|
|
850
|
+
if isinstance(col, Column):
|
|
851
|
+
col_names.append(col.column_name)
|
|
852
|
+
else:
|
|
853
|
+
col_names.append(col)
|
|
854
|
+
self._returning_cols = col_names
|
|
855
|
+
else:
|
|
856
|
+
self._returning_cols = ['*']
|
|
844
857
|
return self
|
|
845
858
|
|
|
846
859
|
def to_tsql(self) -> TSQL:
|
|
@@ -897,7 +910,7 @@ class DeleteBuilder(QueryBuilder):
|
|
|
897
910
|
class SelectQueryBuilder(QueryBuilder):
|
|
898
911
|
"""Fluent interface for building SQL SELECT queries"""
|
|
899
912
|
|
|
900
|
-
def __init__(self, base_table: 'Table'):
|
|
913
|
+
def __init__(self, base_table: type['Table']):
|
|
901
914
|
self.base_table = base_table
|
|
902
915
|
self._columns: Optional[List[Column]] = None
|
|
903
916
|
self._conditions: List[Condition] = []
|
|
@@ -935,16 +948,16 @@ class SelectQueryBuilder(QueryBuilder):
|
|
|
935
948
|
self._conditions.append(condition)
|
|
936
949
|
return self
|
|
937
950
|
|
|
938
|
-
def join(self, table: 'Table', on: Condition, join_type: str = 'INNER') -> 'SelectQueryBuilder':
|
|
951
|
+
def join(self, table: type['Table'], on: Condition, join_type: str = 'INNER') -> 'SelectQueryBuilder':
|
|
939
952
|
"""Add a JOIN clause"""
|
|
940
953
|
self._joins.append(Join(table, on, join_type))
|
|
941
954
|
return self
|
|
942
955
|
|
|
943
|
-
def left_join(self, table: 'Table', on: Condition) -> 'SelectQueryBuilder':
|
|
956
|
+
def left_join(self, table: type['Table'], on: Condition) -> 'SelectQueryBuilder':
|
|
944
957
|
"""Add a LEFT JOIN clause"""
|
|
945
958
|
return self.join(table, on, 'LEFT')
|
|
946
959
|
|
|
947
|
-
def right_join(self, table: 'Table', on: Condition) -> 'SelectQueryBuilder':
|
|
960
|
+
def right_join(self, table: type['Table'], on: Condition) -> 'SelectQueryBuilder':
|
|
948
961
|
"""Add a RIGHT JOIN clause"""
|
|
949
962
|
return self.join(table, on, 'RIGHT')
|
|
950
963
|
|
|
@@ -1083,3 +1096,32 @@ if HAS_SQLALCHEMY:
|
|
|
1083
1096
|
datetime: DateTime,
|
|
1084
1097
|
float: Float,
|
|
1085
1098
|
}
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
# Helper function for type checker compatibility with SQLAlchemy columns
|
|
1102
|
+
def SAColumn(*args: Any, **kwargs: Any) -> Column: # noqa: N802
|
|
1103
|
+
"""Wrapper for SQLAlchemy Column that satisfies type checkers.
|
|
1104
|
+
|
|
1105
|
+
This function returns a SQLAlchemy Column at runtime but tells type checkers
|
|
1106
|
+
it returns a tsql Column. This allows you to use SQLAlchemy columns without
|
|
1107
|
+
explicit type annotations while still getting proper IDE completions.
|
|
1108
|
+
|
|
1109
|
+
Usage:
|
|
1110
|
+
from tsql.query_builder import Table, SAColumn
|
|
1111
|
+
from sqlalchemy import Integer, String
|
|
1112
|
+
|
|
1113
|
+
class Users(Table):
|
|
1114
|
+
id = SAColumn(Integer, primary_key=True) # Type checker sees: tsql Column
|
|
1115
|
+
name = SAColumn(String(100))
|
|
1116
|
+
|
|
1117
|
+
Note: This shadows the SQLAlchemy Column import. Import SA Column explicitly if needed:
|
|
1118
|
+
from sqlalchemy import Column as SA_Column
|
|
1119
|
+
|
|
1120
|
+
Alternative: Use explicit type annotations:
|
|
1121
|
+
from sqlalchemy import Column as SACol
|
|
1122
|
+
id: Column = SACol(Integer, primary_key=True)
|
|
1123
|
+
"""
|
|
1124
|
+
if not HAS_SQLALCHEMY:
|
|
1125
|
+
raise ImportError("SQLAlchemy is not installed. Cannot use SAColumn() helper.")
|
|
1126
|
+
from sqlalchemy import Column as SA_Column
|
|
1127
|
+
return SA_Column(*args, **kwargs) # type: ignore[return-value]
|
|
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
|