t-sql 4.12.0__tar.gz → 4.13.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.12.0/README.md → t_sql-4.13.0/PKG-INFO +71 -0
- t_sql-4.12.0/PKG-INFO → t_sql-4.13.0/README.md +60 -10
- {t_sql-4.12.0 → t_sql-4.13.0}/pyproject.toml +6 -4
- {t_sql-4.12.0 → t_sql-4.13.0}/.dockerignore +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/.github/workflows/publish.yml +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/.github/workflows/test.yml +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/.gitignore +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/Dockerfile +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/LICENSE +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/compose.yaml +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/context7.json +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/pytest.ini +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_alembic_integration.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_deep_nesting.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_different_object_types.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_error_messages.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_escaped.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_helper_functions.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_like_patterns.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_mysql_integration.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_parameter_names.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_query_builder.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_sqlite_integration.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_string_based_builders.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_styles.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_template_in_builders.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_tsql.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tests/test_type_processor.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tsql/__init__.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tsql/query_builder.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tsql/row.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tsql/styles.py +0 -0
- {t_sql-4.12.0 → t_sql-4.13.0}/tsql/type_processor.py +0 -0
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: t-sql
|
|
3
|
+
Version: 4.13.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
|
+
Provides-Extra: sqlalchemy
|
|
9
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'sqlalchemy'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
1
12
|
# t-sql
|
|
2
13
|
|
|
3
14
|
A lightweight SQL templating library that leverages Python 3.14's t-strings (PEP 750).
|
|
@@ -300,6 +311,31 @@ query = (Posts.select()
|
|
|
300
311
|
.left_join(Users, on=Posts.user_id == Users.id))
|
|
301
312
|
```
|
|
302
313
|
|
|
314
|
+
### Raw JOIN clauses (escape hatch)
|
|
315
|
+
|
|
316
|
+
For join shapes the typed `join()`/`left_join()`/`right_join()` can't express —
|
|
317
|
+
LATERAL subqueries, ON-less cross joins, set-returning functions — use
|
|
318
|
+
`join_raw()` to splice a complete JOIN clause Template verbatim. The Template
|
|
319
|
+
must carry the **whole** clause including the join keyword; nothing is added
|
|
320
|
+
around it. Parameters inside the Template are still parameterized and renumber
|
|
321
|
+
correctly alongside the rest of the query.
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
query = (SelectQueryBuilder.from_table('records', schema='dataset', alias='t')
|
|
325
|
+
.select(t't.*')
|
|
326
|
+
.join_raw(t'CROSS JOIN LATERAL unnest(t.tags) AS tag'))
|
|
327
|
+
|
|
328
|
+
# Parameterized raw join, composed with a WHERE clause
|
|
329
|
+
query = (SelectQueryBuilder.from_table('records', alias='t')
|
|
330
|
+
.select(t't.*')
|
|
331
|
+
.join_raw(t'LEFT JOIN other o ON o.id = t.ref AND o.kind = {"x"}')
|
|
332
|
+
.where(t't.active = {True}'))
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
> ⚠️ **Not advised.** `join_raw()` bypasses the builder's join structure. Prefer
|
|
336
|
+
> the typed `join()`/`left_join()`/`right_join()` with a Table + Condition
|
|
337
|
+
> wherever they suffice.
|
|
338
|
+
|
|
303
339
|
## Query Features
|
|
304
340
|
|
|
305
341
|
### Selecting All Columns from a Table
|
|
@@ -323,6 +359,34 @@ query = Posts.select(Posts.ALL, Users.ALL).join(Users, Posts.user_id == Users.id
|
|
|
323
359
|
|
|
324
360
|
This is particularly useful when joining tables where you want all columns from one table but only specific columns from others.
|
|
325
361
|
|
|
362
|
+
### DISTINCT ON (PostgreSQL)
|
|
363
|
+
|
|
364
|
+
`distinct_on()` emits a `SELECT DISTINCT ON (...)` clause. It accepts Column
|
|
365
|
+
objects, string column names, or raw Templates — coerced exactly like
|
|
366
|
+
`select()`.
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
query = Users.select(Users.id, Users.username).distinct_on('username')
|
|
370
|
+
# ('SELECT DISTINCT ON (username) users.id, users.username FROM users', [])
|
|
371
|
+
|
|
372
|
+
# Multiple columns are comma-joined inside the parens
|
|
373
|
+
query = Users.select(Users.id).distinct_on('username', 'email')
|
|
374
|
+
# ('SELECT DISTINCT ON (username, email) users.id FROM users', [])
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### FROM ONLY and Table Aliases
|
|
378
|
+
|
|
379
|
+
When building from a string table name, `from_table()` accepts `only=True` to
|
|
380
|
+
emit `FROM ONLY {table}` (excludes inheriting child tables in PostgreSQL) and
|
|
381
|
+
`alias=...` to emit `FROM {table} AS {alias}` (the alias is identifier-validated):
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
query = (SelectQueryBuilder.from_table('records', schema='dataset', alias='t', only=True)
|
|
385
|
+
.select(t't.*')
|
|
386
|
+
.where(t't.active = {True}'))
|
|
387
|
+
# ('SELECT t.* FROM ONLY dataset.records AS t WHERE (t.active = $1)', [True])
|
|
388
|
+
```
|
|
389
|
+
|
|
326
390
|
### NULL Checks and Other Operators
|
|
327
391
|
|
|
328
392
|
```python
|
|
@@ -349,6 +413,11 @@ query = Posts.select().order_by(Posts.id) # defaults to ASC
|
|
|
349
413
|
query = Posts.select().order_by(Posts.id.desc())
|
|
350
414
|
query = Posts.select().order_by(Posts.created_at.asc(), Posts.id.desc())
|
|
351
415
|
|
|
416
|
+
# ORDER BY / GROUP BY also accept raw Templates, emitted verbatim
|
|
417
|
+
# (parity with where()/having()/select()), for computed expressions:
|
|
418
|
+
query = Posts.select().order_by(t'lower(title) DESC')
|
|
419
|
+
query = Posts.select().group_by(t"date_trunc('day', created_at)")
|
|
420
|
+
|
|
352
421
|
# LIMIT and OFFSET
|
|
353
422
|
query = Posts.select().limit(10).offset(20)
|
|
354
423
|
|
|
@@ -579,6 +648,8 @@ pip install t-sql[sqlalchemy]
|
|
|
579
648
|
uv add t-sql --optional sqlalchemy
|
|
580
649
|
```
|
|
581
650
|
|
|
651
|
+
> Note: the `sqlalchemy` extra installs SQLAlchemy only. Alembic is not a t-sql dependency — if you use alembic autogenerate, add `alembic` to your own project's dependencies (it's your migration tooling, not ours).
|
|
652
|
+
|
|
582
653
|
### Two Ways to Define Columns
|
|
583
654
|
|
|
584
655
|
**1. Simple Column annotations** (for query builder only):
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: t-sql
|
|
3
|
-
Version: 4.12.0
|
|
4
|
-
Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
|
|
5
|
-
Project-URL: Homepage, https://github.com/nhumrich/t-sql
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.14
|
|
8
|
-
Requires-Dist: alembic>=1.17.0
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
|
|
11
1
|
# t-sql
|
|
12
2
|
|
|
13
3
|
A lightweight SQL templating library that leverages Python 3.14's t-strings (PEP 750).
|
|
@@ -310,6 +300,31 @@ query = (Posts.select()
|
|
|
310
300
|
.left_join(Users, on=Posts.user_id == Users.id))
|
|
311
301
|
```
|
|
312
302
|
|
|
303
|
+
### Raw JOIN clauses (escape hatch)
|
|
304
|
+
|
|
305
|
+
For join shapes the typed `join()`/`left_join()`/`right_join()` can't express —
|
|
306
|
+
LATERAL subqueries, ON-less cross joins, set-returning functions — use
|
|
307
|
+
`join_raw()` to splice a complete JOIN clause Template verbatim. The Template
|
|
308
|
+
must carry the **whole** clause including the join keyword; nothing is added
|
|
309
|
+
around it. Parameters inside the Template are still parameterized and renumber
|
|
310
|
+
correctly alongside the rest of the query.
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
query = (SelectQueryBuilder.from_table('records', schema='dataset', alias='t')
|
|
314
|
+
.select(t't.*')
|
|
315
|
+
.join_raw(t'CROSS JOIN LATERAL unnest(t.tags) AS tag'))
|
|
316
|
+
|
|
317
|
+
# Parameterized raw join, composed with a WHERE clause
|
|
318
|
+
query = (SelectQueryBuilder.from_table('records', alias='t')
|
|
319
|
+
.select(t't.*')
|
|
320
|
+
.join_raw(t'LEFT JOIN other o ON o.id = t.ref AND o.kind = {"x"}')
|
|
321
|
+
.where(t't.active = {True}'))
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
> ⚠️ **Not advised.** `join_raw()` bypasses the builder's join structure. Prefer
|
|
325
|
+
> the typed `join()`/`left_join()`/`right_join()` with a Table + Condition
|
|
326
|
+
> wherever they suffice.
|
|
327
|
+
|
|
313
328
|
## Query Features
|
|
314
329
|
|
|
315
330
|
### Selecting All Columns from a Table
|
|
@@ -333,6 +348,34 @@ query = Posts.select(Posts.ALL, Users.ALL).join(Users, Posts.user_id == Users.id
|
|
|
333
348
|
|
|
334
349
|
This is particularly useful when joining tables where you want all columns from one table but only specific columns from others.
|
|
335
350
|
|
|
351
|
+
### DISTINCT ON (PostgreSQL)
|
|
352
|
+
|
|
353
|
+
`distinct_on()` emits a `SELECT DISTINCT ON (...)` clause. It accepts Column
|
|
354
|
+
objects, string column names, or raw Templates — coerced exactly like
|
|
355
|
+
`select()`.
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
query = Users.select(Users.id, Users.username).distinct_on('username')
|
|
359
|
+
# ('SELECT DISTINCT ON (username) users.id, users.username FROM users', [])
|
|
360
|
+
|
|
361
|
+
# Multiple columns are comma-joined inside the parens
|
|
362
|
+
query = Users.select(Users.id).distinct_on('username', 'email')
|
|
363
|
+
# ('SELECT DISTINCT ON (username, email) users.id FROM users', [])
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### FROM ONLY and Table Aliases
|
|
367
|
+
|
|
368
|
+
When building from a string table name, `from_table()` accepts `only=True` to
|
|
369
|
+
emit `FROM ONLY {table}` (excludes inheriting child tables in PostgreSQL) and
|
|
370
|
+
`alias=...` to emit `FROM {table} AS {alias}` (the alias is identifier-validated):
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
query = (SelectQueryBuilder.from_table('records', schema='dataset', alias='t', only=True)
|
|
374
|
+
.select(t't.*')
|
|
375
|
+
.where(t't.active = {True}'))
|
|
376
|
+
# ('SELECT t.* FROM ONLY dataset.records AS t WHERE (t.active = $1)', [True])
|
|
377
|
+
```
|
|
378
|
+
|
|
336
379
|
### NULL Checks and Other Operators
|
|
337
380
|
|
|
338
381
|
```python
|
|
@@ -359,6 +402,11 @@ query = Posts.select().order_by(Posts.id) # defaults to ASC
|
|
|
359
402
|
query = Posts.select().order_by(Posts.id.desc())
|
|
360
403
|
query = Posts.select().order_by(Posts.created_at.asc(), Posts.id.desc())
|
|
361
404
|
|
|
405
|
+
# ORDER BY / GROUP BY also accept raw Templates, emitted verbatim
|
|
406
|
+
# (parity with where()/having()/select()), for computed expressions:
|
|
407
|
+
query = Posts.select().order_by(t'lower(title) DESC')
|
|
408
|
+
query = Posts.select().group_by(t"date_trunc('day', created_at)")
|
|
409
|
+
|
|
362
410
|
# LIMIT and OFFSET
|
|
363
411
|
query = Posts.select().limit(10).offset(20)
|
|
364
412
|
|
|
@@ -589,6 +637,8 @@ pip install t-sql[sqlalchemy]
|
|
|
589
637
|
uv add t-sql --optional sqlalchemy
|
|
590
638
|
```
|
|
591
639
|
|
|
640
|
+
> Note: the `sqlalchemy` extra installs SQLAlchemy only. Alembic is not a t-sql dependency — if you use alembic autogenerate, add `alembic` to your own project's dependencies (it's your migration tooling, not ours).
|
|
641
|
+
|
|
592
642
|
### Two Ways to Define Columns
|
|
593
643
|
|
|
594
644
|
**1. Simple Column annotations** (for query builder only):
|
|
@@ -4,13 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "t-sql"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.13.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"
|
|
11
|
-
dependencies = [
|
|
12
|
-
|
|
13
|
-
]
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
sqlalchemy = ["sqlalchemy>=2.0.0"]
|
|
14
15
|
|
|
15
16
|
[project.urls]
|
|
16
17
|
Homepage = "https://github.com/nhumrich/t-sql"
|
|
@@ -20,6 +21,7 @@ Homepage = "https://github.com/nhumrich/t-sql"
|
|
|
20
21
|
dev = [
|
|
21
22
|
"aiomysql>=0.2.0",
|
|
22
23
|
"aiosqlite>=0.20.0",
|
|
24
|
+
"alembic>=1.17.0",
|
|
23
25
|
"anyio>=4.9.0",
|
|
24
26
|
"asyncpg>=0.30.0",
|
|
25
27
|
"cryptography>=46.0.2",
|
|
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
|
|
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
|