t-sql 1.0.1__tar.gz → 1.1.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-1.1.0/.github/workflows/publish.yml +28 -0
- t_sql-1.1.0/.github/workflows/test.yml +31 -0
- t_sql-1.1.0/PKG-INFO +425 -0
- t_sql-1.1.0/README.md +414 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/pyproject.toml +5 -1
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_injections_for_escaped.py +1 -1
- t_sql-1.1.0/tests/test_query_builder.py +390 -0
- t_sql-1.1.0/tests/test_sqlalchemy_integration.py +245 -0
- t_sql-1.1.0/tsql/query_builder.py +381 -0
- t_sql-1.0.1/.idea/.gitignore +0 -8
- t_sql-1.0.1/.idea/inspectionProfiles/Project_Default.xml +0 -24
- t_sql-1.0.1/.idea/inspectionProfiles/profiles_settings.xml +0 -6
- t_sql-1.0.1/.idea/misc.xml +0 -11
- t_sql-1.0.1/.idea/tsql.iml +0 -10
- t_sql-1.0.1/.idea/vcs.xml +0 -6
- t_sql-1.0.1/.idea/workspace.xml +0 -92
- t_sql-1.0.1/PKG-INFO +0 -182
- t_sql-1.0.1/README.md +0 -173
- {t_sql-1.0.1 → t_sql-1.1.0}/.dockerignore +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/.gitignore +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/Dockerfile +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/LICENSE +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/compose.yaml +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/pytest.ini +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_different_object_types.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_escaped.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_helper_functions.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_styles.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tests/test_tsql.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tsql/__init__.py +0 -0
- {t_sql-1.0.1 → t_sql-1.1.0}/tsql/styles.py +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.14"
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v4
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: uv build
|
|
26
|
+
|
|
27
|
+
- name: Publish to PyPI
|
|
28
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.14"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v4
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: uv run pytest -v
|
t_sql-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: t-sql
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
|
|
5
|
+
Project-URL: Homepage, https://github.com/nhumrich/tsql
|
|
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
|
+
|
|
12
|
+
# tsql
|
|
13
|
+
|
|
14
|
+
A lightweight SQL templating library that leverages Python 3.14's t-strings (PEP 750).
|
|
15
|
+
|
|
16
|
+
TSQL provides a safe way to write SQL queries using Python's template strings (t-strings) while preventing SQL injection attacks through multiple parameter styling options.
|
|
17
|
+
|
|
18
|
+
## ⚠️ Python Version Requirement
|
|
19
|
+
This library requires Python 3.14+
|
|
20
|
+
|
|
21
|
+
TSQL is built specifically to take advantage of the new t-string feature introduced in PEP 750, which is only available in Python 3.14+.
|
|
22
|
+
|
|
23
|
+
## Installing
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
# with pip
|
|
27
|
+
pip install t-sql
|
|
28
|
+
|
|
29
|
+
# with uv
|
|
30
|
+
uv add t-sql
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## using
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
import tsql
|
|
37
|
+
|
|
38
|
+
tsql.render(t"select * from users where name={name)")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Parameter Styles
|
|
42
|
+
|
|
43
|
+
- **QMARK** (default): Uses `?` placeholders
|
|
44
|
+
- **NUMERIC**: Uses `:1`, `:2`, etc. placeholders
|
|
45
|
+
- **NAMED**: Uses `:name` placeholders
|
|
46
|
+
- **FORMAT**: Uses `%s` placeholders
|
|
47
|
+
- **PYFORMAT**: Uses `%(name)s` placeholders
|
|
48
|
+
- **NUMERIC_DOLLAR**: Uses `$1`, `$2`, etc. (PostgreSQL native)
|
|
49
|
+
- **ESCAPED**: Escapes values directly into SQL (no parameters)
|
|
50
|
+
|
|
51
|
+
## Examples:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
|
|
55
|
+
# Basic usage with different parameter styles
|
|
56
|
+
import tsql
|
|
57
|
+
import tsql.styles
|
|
58
|
+
|
|
59
|
+
name = 'billy'
|
|
60
|
+
query = t'select * from users where name={name}'
|
|
61
|
+
|
|
62
|
+
# Default QMARK style
|
|
63
|
+
print(tsql.render(query))
|
|
64
|
+
# ('select * from users where name = ?', ['billy'])
|
|
65
|
+
|
|
66
|
+
# PostgreSQL native style
|
|
67
|
+
print(tsql.render(query, style=tsql.styles.NUMERIC_DOLLAR))
|
|
68
|
+
# ('select * from users where name = $1', ['billy'])
|
|
69
|
+
|
|
70
|
+
# ESCAPED style (no parameters)
|
|
71
|
+
print(tsql.render(query, style=tsql.styles.ESCAPED))
|
|
72
|
+
# ("select * from users where name = 'billy'", [])
|
|
73
|
+
|
|
74
|
+
# SQL injection prevention
|
|
75
|
+
name = "billy ' and 1=1 --"
|
|
76
|
+
print(tsql.render(query, style=tsql.styles.ESCAPED))
|
|
77
|
+
# ("select * from users where name = 'billy '' and 1=1 --'", [])
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Format-spec helpers
|
|
82
|
+
|
|
83
|
+
There are some built-in format spec helpers that can change the way some
|
|
84
|
+
parts of the library work.
|
|
85
|
+
|
|
86
|
+
### Literal
|
|
87
|
+
One common example is you may want to set the name
|
|
88
|
+
of a column dynamically. By using the `literal` format spec, the value will
|
|
89
|
+
be sanitized against a valid literal and put straight into the sql query since
|
|
90
|
+
you cannot parameterize that part of a query, example:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
query = t'select * from {table:literal} where {col:literal}={val}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
or, a full example:
|
|
97
|
+
```python
|
|
98
|
+
|
|
99
|
+
# with a like clause
|
|
100
|
+
min_age = 30
|
|
101
|
+
search_column = "name"
|
|
102
|
+
pattern = "O'Brien"
|
|
103
|
+
is_active = True
|
|
104
|
+
tsql.render(t"SELECT * FROM test_users WHERE age >= {min_age} AND {search_column:literal} LIKE '%' || {pattern} || '%' AND active = {is_active}")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### unsafe
|
|
108
|
+
You may want to do advanced things that may otherwise be considered unsfe.
|
|
109
|
+
This is okay if you can be sure that a user is not providing input. Like maybe
|
|
110
|
+
you care storing a query for some reason.
|
|
111
|
+
As per the name, this can open you up to sql injection and should be used with
|
|
112
|
+
extreme caution.
|
|
113
|
+
You can use the "unsafe" format spec for these
|
|
114
|
+
cases:
|
|
115
|
+
```python
|
|
116
|
+
dynamic_where = input('type where clause')
|
|
117
|
+
tsql.render(t"SELECT * FROM users WHERE {dynamic_where:unsafe}")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### as_values
|
|
121
|
+
|
|
122
|
+
The spec `:as_values` formats a dictionary into the format:
|
|
123
|
+
`(key1, key2, ...) VALUES (value1, value2, ...)` for uses in insert statements.
|
|
124
|
+
|
|
125
|
+
### as_set
|
|
126
|
+
|
|
127
|
+
The spec `:as_set` formats a dictionary into the format:
|
|
128
|
+
`key1='?', key2='?'` for uses in update statements.
|
|
129
|
+
|
|
130
|
+
### traditional format_spec
|
|
131
|
+
|
|
132
|
+
All other format specs should be handled as they would in a normal f-string.
|
|
133
|
+
|
|
134
|
+
## Included helper methods
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# select
|
|
138
|
+
tsql.select('table', 'abc123')
|
|
139
|
+
# SELECT * FROM table WHERE id='abc123'
|
|
140
|
+
|
|
141
|
+
# select with multiple ids and specific columns
|
|
142
|
+
tsql.select('users', ['abc123', 'def456'], columns=['name', 'age'])
|
|
143
|
+
# SELECT name, age FROM users WHERE id in ('abc123', 'def456')
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# t_join (joins multiple t-strings together like .join on a str)
|
|
147
|
+
tsql.t_join(t" ", [t"hello", t"there"])
|
|
148
|
+
# t"hello there"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# insert
|
|
152
|
+
table = 'users'
|
|
153
|
+
values = {'id': 'abc123', 'name': 'bob', 'email': 'bob@example.com'}
|
|
154
|
+
tsql.insert(table, values)
|
|
155
|
+
# INSERT INTO users (id, name, email) VALUES ('abc123', 'bob', 'bob@example.com')
|
|
156
|
+
|
|
157
|
+
# update values on a single row
|
|
158
|
+
table = 'users'
|
|
159
|
+
values = {'name': 'joe', 'email': 'joe@example.com'}
|
|
160
|
+
tsql.update(table, values, id='abc123')
|
|
161
|
+
# UPDATE users SET name='joe', email='joe@example.com' WHERE id='abc123'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
# Query Builder
|
|
165
|
+
|
|
166
|
+
For a more structured approach to building queries, TSQL includes an optional query builder that provides a fluent interface with type-safe column references.
|
|
167
|
+
|
|
168
|
+
## Basic Usage
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from tsql.query_builder import table
|
|
172
|
+
|
|
173
|
+
@table('users')
|
|
174
|
+
class Users:
|
|
175
|
+
id: int
|
|
176
|
+
username: str
|
|
177
|
+
email: str
|
|
178
|
+
created_at: str
|
|
179
|
+
|
|
180
|
+
# Decorator returns instance - use directly!
|
|
181
|
+
query = Users.select(Users.id, Users.username)
|
|
182
|
+
sql, params = query.render()
|
|
183
|
+
# SELECT users.id, users.username FROM users
|
|
184
|
+
|
|
185
|
+
# With WHERE clause
|
|
186
|
+
query = Users.select().where(Users.id > 100)
|
|
187
|
+
sql, params = query.render()
|
|
188
|
+
# SELECT * FROM users WHERE users.id > ?
|
|
189
|
+
# params: [100]
|
|
190
|
+
|
|
191
|
+
# Multiple WHERE conditions (ANDed together)
|
|
192
|
+
query = (Users.select(Users.username, Users.email)
|
|
193
|
+
.where(Users.id > 10)
|
|
194
|
+
.where(Users.email != None))
|
|
195
|
+
sql, params = query.render()
|
|
196
|
+
# SELECT users.username, users.email FROM users WHERE users.id > ? AND users.email IS NOT NULL
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Joins
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
@table('posts')
|
|
203
|
+
class Posts:
|
|
204
|
+
id: int
|
|
205
|
+
user_id: int
|
|
206
|
+
title: str
|
|
207
|
+
body: str
|
|
208
|
+
|
|
209
|
+
@table('comments')
|
|
210
|
+
class Comments:
|
|
211
|
+
id: int
|
|
212
|
+
post_id: int
|
|
213
|
+
user_id: int
|
|
214
|
+
content: str
|
|
215
|
+
|
|
216
|
+
# INNER JOIN
|
|
217
|
+
query = (Posts.select(Posts.title, Users.username)
|
|
218
|
+
.join(Users, Posts.user_id == Users.id)
|
|
219
|
+
.where(Posts.id > 100))
|
|
220
|
+
sql, params = query.render()
|
|
221
|
+
# SELECT posts.title, users.username FROM posts INNER JOIN users ON posts.user_id = users.id WHERE posts.id > ?
|
|
222
|
+
|
|
223
|
+
# LEFT JOIN with multiple tables
|
|
224
|
+
query = (Comments.select(Comments.content, Posts.title, Users.username)
|
|
225
|
+
.join(Posts, Comments.post_id == Posts.id)
|
|
226
|
+
.left_join(Users, Comments.user_id == Users.id))
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Additional Features
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
# IN clause
|
|
233
|
+
query = Users.select().where(Users.id.in_([1, 2, 3]))
|
|
234
|
+
|
|
235
|
+
# LIKE clause
|
|
236
|
+
query = Users.select().where(Users.username.like('%john%'))
|
|
237
|
+
|
|
238
|
+
# ORDER BY
|
|
239
|
+
query = Posts.select().order_by(Posts.created_at)
|
|
240
|
+
query = Posts.select().order_by((Posts.id, 'DESC'))
|
|
241
|
+
|
|
242
|
+
# LIMIT
|
|
243
|
+
query = Posts.select().limit(10)
|
|
244
|
+
|
|
245
|
+
# Complex query
|
|
246
|
+
query = (Posts.select(Posts.title, Users.username)
|
|
247
|
+
.join(Users, Posts.user_id == Users.id)
|
|
248
|
+
.where(Posts.id > 100)
|
|
249
|
+
.where(Users.id >= 5)
|
|
250
|
+
.order_by((Posts.id, 'DESC'))
|
|
251
|
+
.limit(20))
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Advanced Mixed Query (Query Builder + T-Strings)
|
|
255
|
+
|
|
256
|
+
You can combine the query builder's structured approach with raw t-string conditions for complex logic:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from tsql.query_builder import table
|
|
260
|
+
from sqlalchemy import MetaData, Column, String, Integer
|
|
261
|
+
|
|
262
|
+
metadata = MetaData()
|
|
263
|
+
|
|
264
|
+
@table('users', metadata=metadata)
|
|
265
|
+
class Users:
|
|
266
|
+
id = Column(String, primary_key=True)
|
|
267
|
+
name = Column(String)
|
|
268
|
+
age = Column(Integer)
|
|
269
|
+
email = Column(String)
|
|
270
|
+
|
|
271
|
+
# Start with query builder for the base query
|
|
272
|
+
query = Users.select(Users.id, Users.name, Users.email)
|
|
273
|
+
|
|
274
|
+
# Add simple conditions with query builder
|
|
275
|
+
query = query.where(Users.age > 18)
|
|
276
|
+
|
|
277
|
+
# Add complex logic with t-strings for advanced conditions
|
|
278
|
+
search_term = "john"
|
|
279
|
+
min_age = 25
|
|
280
|
+
name_col = str(Users.name)
|
|
281
|
+
email_col = str(Users.email)
|
|
282
|
+
age_col = str(Users.age)
|
|
283
|
+
|
|
284
|
+
# Build advanced t-string condition with OR logic
|
|
285
|
+
advanced_condition = t"{name_col:literal} LIKE '%' || {search_term} || '%' OR {email_col:literal} LIKE '%' || {search_term} || '%'"
|
|
286
|
+
|
|
287
|
+
# Mix it into the query builder (t-string conditions are automatically wrapped in parentheses)
|
|
288
|
+
query = query.where(advanced_condition)
|
|
289
|
+
|
|
290
|
+
sql, params = query.render()
|
|
291
|
+
# SELECT users.id, users.name, users.email FROM users
|
|
292
|
+
# WHERE users.age > ? AND (users.name LIKE '%' || ? || '%' OR users.email LIKE '%' || ? || '%')
|
|
293
|
+
# params: [18, 'john', 'john']
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Important:** When using t-string conditions with `.where()`, they are automatically wrapped in parentheses to ensure proper operator precedence when combined with other conditions using AND. This prevents issues when your t-string contains OR operators.
|
|
297
|
+
|
|
298
|
+
This approach lets you use the query builder for structure and safety, while dropping down to t-strings when you need custom SQL logic that the query builder doesn't support.
|
|
299
|
+
|
|
300
|
+
### Schema Support
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
@table('users', schema='public')
|
|
304
|
+
class Users:
|
|
305
|
+
id: int
|
|
306
|
+
name: str
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## SQLAlchemy & Alembic Integration
|
|
310
|
+
|
|
311
|
+
The query builder can integrate with SQLAlchemy's metadata system, allowing alembic autogenerate to work while maintaining the clean query builder syntax.
|
|
312
|
+
|
|
313
|
+
First, install with SQLAlchemy support:
|
|
314
|
+
```bash
|
|
315
|
+
pip install t-sql[sqlalchemy]
|
|
316
|
+
# or
|
|
317
|
+
uv add t-sql --optional sqlalchemy
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Two Ways to Define Columns
|
|
321
|
+
|
|
322
|
+
**1. Simple type annotations** (no alembic needed):
|
|
323
|
+
```python
|
|
324
|
+
from tsql.query_builder import table
|
|
325
|
+
|
|
326
|
+
@table('users') # No metadata = query builder only
|
|
327
|
+
class Users:
|
|
328
|
+
id: int
|
|
329
|
+
name: str
|
|
330
|
+
age: int
|
|
331
|
+
|
|
332
|
+
# Decorator returns an instance - no need to instantiate!
|
|
333
|
+
query = Users.select(Users.name).where(Users.age > 18)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**2. SQLAlchemy Column objects** (for alembic integration - **recommended**):
|
|
337
|
+
```python
|
|
338
|
+
from sqlalchemy import MetaData, Column, String, Integer, ForeignKey, TIMESTAMP
|
|
339
|
+
from sqlalchemy.sql.functions import now
|
|
340
|
+
from tsql.query_builder import table
|
|
341
|
+
|
|
342
|
+
metadata = MetaData()
|
|
343
|
+
|
|
344
|
+
@table('users', metadata=metadata)
|
|
345
|
+
class Users:
|
|
346
|
+
id = Column(String, primary_key=True, default=lambda: gen_id())
|
|
347
|
+
email = Column(String(255), unique=True, nullable=False, index=True)
|
|
348
|
+
name = Column(String(100))
|
|
349
|
+
created_ts = Column(TIMESTAMP(timezone=True), server_default=now(), nullable=False)
|
|
350
|
+
|
|
351
|
+
@table('posts', metadata=metadata)
|
|
352
|
+
class Posts:
|
|
353
|
+
id = Column(String, primary_key=True)
|
|
354
|
+
user_id = Column(String, ForeignKey('users.id', ondelete='CASCADE'), index=True)
|
|
355
|
+
title = Column(String(500))
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**You can mix both approaches**:
|
|
359
|
+
```python
|
|
360
|
+
@table('events', metadata=metadata)
|
|
361
|
+
class Events:
|
|
362
|
+
id = Column(String, primary_key=True, default=lambda: gen_id("e")) # Full SA
|
|
363
|
+
topic: str # Simple - becomes nullable String column
|
|
364
|
+
created_ts = Column(TIMESTAMP(timezone=True), server_default=now())
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Using the Query Builder
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
# For alembic (in your models.py or env.py)
|
|
371
|
+
target_metadata = metadata
|
|
372
|
+
|
|
373
|
+
# For queries - use decorated classes directly (they're already instances!)
|
|
374
|
+
query = (Posts.select(Posts.title, Users.name)
|
|
375
|
+
.join(Users, Posts.user_id == Users.id)
|
|
376
|
+
.where(Users.age > 18))
|
|
377
|
+
|
|
378
|
+
sql, params = query.render()
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Why Use SQLAlchemy Column?
|
|
382
|
+
|
|
383
|
+
Using `Column(...)` directly gives you:
|
|
384
|
+
- ✅ Full SQLAlchemy feature support (server defaults, computed columns, custom types, etc.)
|
|
385
|
+
- ✅ `ondelete` cascade rules on foreign keys
|
|
386
|
+
- ✅ Custom types like `JSONB`, `TIMESTAMP(timezone=True)`, `TypeDecorator`
|
|
387
|
+
- ✅ Callable defaults: `default=lambda: gen_id()`
|
|
388
|
+
- ✅ Server defaults: `server_default=now()`
|
|
389
|
+
- ✅ Column comments
|
|
390
|
+
- ✅ Everything SQLAlchemy supports
|
|
391
|
+
|
|
392
|
+
### How It Works
|
|
393
|
+
|
|
394
|
+
When you provide a `metadata` parameter to `@table()`, the decorator:
|
|
395
|
+
1. Detects SQLAlchemy `Column` objects and uses them directly
|
|
396
|
+
2. Creates query builder Column descriptors for fluent syntax
|
|
397
|
+
3. Registers tables to metadata for alembic
|
|
398
|
+
|
|
399
|
+
This means:
|
|
400
|
+
- Alembic autogenerate works perfectly
|
|
401
|
+
- Query builder gives you type-safe queries
|
|
402
|
+
- Single source of truth for your schema
|
|
403
|
+
- SQLAlchemy is optional - query builder works standalone
|
|
404
|
+
|
|
405
|
+
# Note on usage
|
|
406
|
+
|
|
407
|
+
This library should ideally be used inside middleware or library code
|
|
408
|
+
right before making an actual query. It can be used to enforce
|
|
409
|
+
using t-strings and prevent using raw strings.
|
|
410
|
+
|
|
411
|
+
For example:
|
|
412
|
+
|
|
413
|
+
```
|
|
414
|
+
from string.templatelib import Template
|
|
415
|
+
|
|
416
|
+
import tsql
|
|
417
|
+
|
|
418
|
+
def execute_sql_query(query):
|
|
419
|
+
if not isinstance(query, Template):
|
|
420
|
+
raise TypeError('Cannot make a query without using t-strings')
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
return sql_engine.execute(*tsql.render(query))
|
|
424
|
+
|
|
425
|
+
```
|