t-sql 1.2.1__tar.gz → 2.0.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.
Files changed (40) hide show
  1. {t_sql-1.2.1 → t_sql-2.0.0}/.github/workflows/test.yml +20 -0
  2. t_sql-2.0.0/PKG-INFO +530 -0
  3. t_sql-2.0.0/README.md +521 -0
  4. t_sql-2.0.0/compose.yaml +29 -0
  5. {t_sql-1.2.1 → t_sql-2.0.0}/pyproject.toml +5 -5
  6. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_asyncpg_integration.py +1 -92
  7. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_helper_functions.py +0 -55
  8. t_sql-2.0.0/tests/test_mysql_integration.py +291 -0
  9. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_query_builder.py +363 -50
  10. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_sqlalchemy_integration.py +20 -31
  11. t_sql-2.0.0/tests/test_sqlite_integration.py +357 -0
  12. {t_sql-1.2.1 → t_sql-2.0.0}/tsql/__init__.py +44 -61
  13. t_sql-2.0.0/tsql/query_builder.py +822 -0
  14. t_sql-1.2.1/.idea/.gitignore +0 -8
  15. t_sql-1.2.1/.idea/inspectionProfiles/Project_Default.xml +0 -24
  16. t_sql-1.2.1/.idea/inspectionProfiles/profiles_settings.xml +0 -6
  17. t_sql-1.2.1/.idea/misc.xml +0 -11
  18. t_sql-1.2.1/.idea/tsql.iml +0 -10
  19. t_sql-1.2.1/.idea/vcs.xml +0 -6
  20. t_sql-1.2.1/.idea/workspace.xml +0 -118
  21. t_sql-1.2.1/PKG-INFO +0 -491
  22. t_sql-1.2.1/README.md +0 -480
  23. t_sql-1.2.1/compose.yaml +0 -14
  24. t_sql-1.2.1/tsql/query_builder.py +0 -532
  25. {t_sql-1.2.1 → t_sql-2.0.0}/.dockerignore +0 -0
  26. {t_sql-1.2.1 → t_sql-2.0.0}/.github/workflows/publish.yml +0 -0
  27. {t_sql-1.2.1 → t_sql-2.0.0}/.gitignore +0 -0
  28. {t_sql-1.2.1 → t_sql-2.0.0}/Dockerfile +0 -0
  29. {t_sql-1.2.1 → t_sql-2.0.0}/LICENSE +0 -0
  30. {t_sql-1.2.1 → t_sql-2.0.0}/context7.json +0 -0
  31. {t_sql-1.2.1 → t_sql-2.0.0}/pytest.ini +0 -0
  32. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_different_object_types.py +0 -0
  33. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_escaped.py +0 -0
  34. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_escaped_binary_hex.py +0 -0
  35. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_injection_edge_cases.py +0 -0
  36. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_injection_protection_validation.py +0 -0
  37. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_injections_for_escaped.py +0 -0
  38. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_styles.py +0 -0
  39. {t_sql-1.2.1 → t_sql-2.0.0}/tests/test_tsql.py +0 -0
  40. {t_sql-1.2.1 → t_sql-2.0.0}/tsql/styles.py +0 -0
@@ -28,6 +28,21 @@ jobs:
28
28
  --health-timeout 5s
29
29
  --health-retries 5
30
30
 
31
+ mysql:
32
+ image: mysql:8.0
33
+ env:
34
+ MYSQL_ROOT_PASSWORD: password
35
+ MYSQL_DATABASE: testdb
36
+ MYSQL_USER: testuser
37
+ MYSQL_PASSWORD: password
38
+ ports:
39
+ - 3306:3306
40
+ options: >-
41
+ --health-cmd "mysqladmin ping -h localhost -u root -ppassword"
42
+ --health-interval 10s
43
+ --health-timeout 5s
44
+ --health-retries 5
45
+
31
46
  steps:
32
47
  - uses: actions/checkout@v4
33
48
 
@@ -45,4 +60,9 @@ jobs:
45
60
  - name: Run tests
46
61
  env:
47
62
  DATABASE_URL: postgresql://postgres:password@localhost:5432/postgres
63
+ MYSQL_HOST: localhost
64
+ MYSQL_PORT: 3306
65
+ MYSQL_USER: testuser
66
+ MYSQL_PASSWORD: password
67
+ MYSQL_DB: testdb
48
68
  run: uv run pytest -v
t_sql-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,530 @@
1
+ Metadata-Version: 2.4
2
+ Name: t-sql
3
+ Version: 2.0.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
+ Description-Content-Type: text/markdown
9
+
10
+ # t-sql
11
+
12
+ A lightweight SQL templating library that leverages Python 3.14's t-strings (PEP 750).
13
+ (Note: This library has absolutely nothing to do with Microsoft SQLServer)
14
+
15
+ t-sql 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.
16
+
17
+ ## ⚠️ Python Version Requirement
18
+ This library requires Python 3.14+
19
+
20
+ t-sql is built specifically to take advantage of the new t-string feature introduced in PEP 750, which is only available in Python 3.14+.
21
+
22
+ ## Installing
23
+
24
+ ```bash
25
+ # with pip
26
+ pip install t-sql
27
+
28
+ # with uv
29
+ uv add t-sql
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ import tsql
36
+
37
+ # Basic usage
38
+ name = 'billy'
39
+ query = t'select * from users where name={name}'
40
+
41
+ # Render with default QMARK style
42
+ sql, params = tsql.render(query)
43
+ # ('select * from users where name = ?', ['billy'])
44
+
45
+ # Or use a different parameter style
46
+ sql, params = tsql.render(query, style=tsql.styles.NUMERIC_DOLLAR)
47
+ # ('select * from users where name = $1', ['billy'])
48
+ ```
49
+
50
+ ## Parameter Styles
51
+
52
+ - **QMARK** (default): Uses `?` placeholders
53
+ - **NUMERIC**: Uses `:1`, `:2`, etc. placeholders
54
+ - **NAMED**: Uses `:name` placeholders
55
+ - **FORMAT**: Uses `%s` placeholders
56
+ - **PYFORMAT**: Uses `%(name)s` placeholders
57
+ - **NUMERIC_DOLLAR**: Uses `$1`, `$2`, etc. (PostgreSQL native)
58
+ - **ESCAPED**: Escapes values directly into SQL (no parameters)
59
+
60
+ ## Core Features
61
+
62
+ ### SQL Injection Prevention
63
+
64
+ ```python
65
+ # SQL injection prevention works automatically
66
+ name = "billy ' and 1=1 --"
67
+ sql, params = tsql.render(t'select * from users where name={name}')
68
+ # Even with ESCAPED style, quotes are properly escaped
69
+ sql, _ = tsql.render(t'select * from users where name={name}', style=tsql.styles.ESCAPED)
70
+ # ("select * from users where name = 'billy '' and 1=1 --'", [])
71
+ ```
72
+
73
+ ### Format-spec helpers
74
+
75
+ #### Literal
76
+
77
+ For table/column names that can't be parameterized:
78
+
79
+ ```python
80
+ table = "users"
81
+ col = "name"
82
+ val = "billy"
83
+ query = t'select * from {table:literal} where {col:literal}={val}'
84
+ sql, params = tsql.render(query)
85
+ # ('select * from users where name = ?', ['billy'])
86
+ ```
87
+
88
+ #### unsafe
89
+
90
+ For cases where you need to bypass safety (use with extreme caution):
91
+
92
+ ```python
93
+ dynamic_where = "age > 18 AND active = true"
94
+ sql, params = tsql.render(t"SELECT * FROM users WHERE {dynamic_where:unsafe}")
95
+ ```
96
+
97
+ #### as_values
98
+
99
+ Formats a dictionary for INSERT statements:
100
+
101
+ ```python
102
+ values = {'id': 'abc123', 'name': 'bob', 'email': 'bob@example.com'}
103
+ sql, params = tsql.render(t"INSERT INTO users {values:as_values}")
104
+ # ('INSERT INTO users (id, name, email) VALUES (?, ?, ?)', ['abc123', 'bob', 'bob@example.com'])
105
+ ```
106
+
107
+ #### as_set
108
+
109
+ Formats a dictionary for UPDATE statements:
110
+
111
+ ```python
112
+ values = {'name': 'joe', 'email': 'joe@example.com'}
113
+ sql, params = tsql.render(t"UPDATE users SET {values:as_set} WHERE id='abc123'")
114
+ # ('UPDATE users SET name = ?, email = ? WHERE id='abc123'', ['joe', 'joe@example.com'])
115
+ ```
116
+
117
+ ### Helper Functions
118
+
119
+ t-sql provides several convenience functions for common SQL operations:
120
+
121
+ #### t_join
122
+
123
+ Joins multiple t-strings together:
124
+
125
+ ```python
126
+ import tsql
127
+
128
+ min_age = 18
129
+ parts = [t"SELECT *", t"FROM users", t"WHERE age > {min_age}"]
130
+ query = tsql.t_join(t" ", parts)
131
+ sql, params = tsql.render(query)
132
+ # ('SELECT * FROM users WHERE age > ?', [18])
133
+ ```
134
+
135
+ #### select
136
+
137
+ Quick SELECT queries:
138
+
139
+ ```python
140
+ # Select all columns
141
+ query = tsql.select('users')
142
+ sql, params = query.render()
143
+ # ('SELECT * FROM users', [])
144
+
145
+ # Select specific columns
146
+ query = tsql.select('users', columns=['name', 'email'])
147
+ sql, params = query.render()
148
+ # ('SELECT name, email FROM users', [])
149
+
150
+ # With WHERE clause
151
+ query = tsql.select('users', columns=['name', 'email'], where={'age': 18})
152
+ sql, params = query.render()
153
+ # ('SELECT name, email FROM users WHERE age = ?', [18])
154
+ ```
155
+
156
+ #### insert
157
+
158
+ Quick INSERT queries:
159
+
160
+ ```python
161
+ values = {'id': 'abc123', 'name': 'bob', 'email': 'bob@example.com'}
162
+ query = tsql.insert('users', values)
163
+ sql, params = query.render()
164
+ # ('INSERT INTO users (id, name, email) VALUES (?, ?, ?)', ['abc123', 'bob', 'bob@example.com'])
165
+ ```
166
+
167
+ #### update
168
+
169
+ Quick UPDATE queries:
170
+
171
+ ```python
172
+ # Update by ID
173
+ query = tsql.update('users', {'email': 'new@example.com'}, id_value='abc123')
174
+ sql, params = query.render()
175
+ # ('UPDATE users SET email = ? WHERE id = ?', ['new@example.com', 'abc123'])
176
+
177
+ # Update with custom WHERE
178
+ query = tsql.update('users', {'email': 'new@example.com'}, where={'age': 25})
179
+ sql, params = query.render()
180
+ # ('UPDATE users SET email = ? WHERE age = ?', ['new@example.com', 25])
181
+ ```
182
+
183
+ #### delete
184
+
185
+ Quick DELETE queries:
186
+
187
+ ```python
188
+ # Delete by ID
189
+ query = tsql.delete('users', id_value='abc123')
190
+ sql, params = query.render()
191
+ # ('DELETE FROM users WHERE id = ?', ['abc123'])
192
+
193
+ # Delete with custom WHERE
194
+ query = tsql.delete('users', where={'age': 18})
195
+ sql, params = query.render()
196
+ # ('DELETE FROM users WHERE age = ?', [18])
197
+ ```
198
+
199
+ **Note:** These helper functions return query builder objects, so you can chain additional methods:
200
+
201
+ ```python
202
+ query = tsql.select('users').where(t'age > {min_age}').limit(10)
203
+ sql, params = query.render()
204
+ ```
205
+
206
+ # Query Builder
207
+
208
+ For a more structured approach, t-sql includes an optional query builder with a fluent interface and type-safe column references.
209
+
210
+ ## Basic Usage
211
+
212
+ ```python
213
+ from tsql.query_builder import Table, Column
214
+
215
+ class Users(Table):
216
+ id: Column
217
+ username: Column
218
+ email: Column
219
+ age: Column
220
+
221
+ # Simple SELECT
222
+ query = Users.select(Users.id, Users.username)
223
+ sql, params = query.render()
224
+ # ('SELECT users.id, users.username FROM users', [])
225
+
226
+ # With WHERE clause
227
+ query = Users.select().where(Users.age > 18)
228
+ sql, params = query.render()
229
+ # ('SELECT * FROM users WHERE users.age > ?', [18])
230
+
231
+ # Multiple conditions (ANDed together)
232
+ query = (Users.select(Users.username, Users.email)
233
+ .where(Users.age > 18)
234
+ .where(Users.email != None))
235
+ ```
236
+
237
+ **Table Names:** The table name defaults to the lowercase class name. To specify a custom name:
238
+
239
+ ```python
240
+ class UserAccount(Table, table_name='user_accounts'):
241
+ id: Column
242
+ username: Column
243
+ ```
244
+
245
+ ## Joins
246
+
247
+ ```python
248
+ class Posts(Table):
249
+ id: Column
250
+ user_id: Column
251
+ title: Column
252
+
253
+ # INNER JOIN
254
+ query = (Posts.select(Posts.title, Users.username)
255
+ .join(Users, on=Posts.user_id == Users.id)
256
+ .where(Posts.id > 100))
257
+
258
+ # LEFT JOIN
259
+ query = (Posts.select()
260
+ .left_join(Users, on=Posts.user_id == Users.id))
261
+ ```
262
+
263
+ ## Query Features
264
+
265
+ ```python
266
+ # IN clause
267
+ query = Users.select().where(Users.id.in_([1, 2, 3]))
268
+
269
+ # LIKE clause
270
+ query = Users.select().where(Users.username.like('%john%'))
271
+
272
+ # ORDER BY
273
+ query = Posts.select().order_by(Posts.id)
274
+ query = Posts.select().order_by((Posts.id, 'DESC'))
275
+
276
+ # LIMIT and OFFSET
277
+ query = Posts.select().limit(10).offset(20)
278
+
279
+ # GROUP BY and HAVING
280
+ query = (Posts.select()
281
+ .group_by(Posts.user_id)
282
+ .having(t'COUNT(*) > {min_count}'))
283
+ ```
284
+
285
+ ## Write Operations
286
+
287
+ The query builder supports INSERT, UPDATE, and DELETE with database-agnostic conflict handling.
288
+
289
+ ### INSERT
290
+
291
+ ```python
292
+ # Basic insert
293
+ values = {'id': 'abc123', 'username': 'john', 'email': 'john@example.com'}
294
+ query = Users.insert(values)
295
+ sql, params = query.render()
296
+ # ('INSERT INTO users (id, username, email) VALUES (?, ?, ?)', ['abc123', 'john', 'john@example.com'])
297
+
298
+ # INSERT with RETURNING (Postgres/SQLite)
299
+ query = Users.insert(values).returning()
300
+ sql, params = query.render()
301
+ # ('INSERT INTO users (id, username, email) VALUES (?, ?, ?) RETURNING *', [...])
302
+
303
+ # INSERT IGNORE (MySQL)
304
+ query = Users.insert(values).ignore()
305
+ sql, params = query.render()
306
+ # ('INSERT IGNORE INTO users (id, username, email) VALUES (?, ?, ?)', [...])
307
+
308
+ # ON CONFLICT DO NOTHING (Postgres/SQLite)
309
+ query = Users.insert(values).on_conflict_do_nothing()
310
+ # ('INSERT INTO users (...) VALUES (...) ON CONFLICT DO NOTHING', [...])
311
+
312
+ # ON CONFLICT DO NOTHING with specific conflict target (Postgres/SQLite)
313
+ query = Users.insert(values).on_conflict_do_nothing(conflict_on='email')
314
+ # ('INSERT INTO users (...) VALUES (...) ON CONFLICT (email) DO NOTHING', [...])
315
+
316
+ # ON CONFLICT DO UPDATE (Postgres/SQLite upsert)
317
+ query = Users.insert(values).on_conflict_update(conflict_on='id')
318
+ # ('INSERT INTO users (...) VALUES (...)
319
+ # ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, email = EXCLUDED.email', [...])
320
+
321
+ # ON CONFLICT with custom update
322
+ query = Users.insert(values).on_conflict_update(
323
+ conflict_on='id',
324
+ update={'username': 'updated_name'}
325
+ )
326
+
327
+ # ON DUPLICATE KEY UPDATE (MySQL)
328
+ query = Users.insert(values).on_duplicate_key_update()
329
+ # ('INSERT INTO users (...) VALUES (...)
330
+ # ON DUPLICATE KEY UPDATE id = VALUES(id), username = VALUES(username), ...', [...])
331
+
332
+ # Chain multiple modifiers
333
+ query = (Users.insert(values)
334
+ .on_conflict_update(conflict_on='id')
335
+ .returning('id', 'username'))
336
+ ```
337
+
338
+ ### UPDATE
339
+
340
+ ```python
341
+ # Basic update (no WHERE = updates all rows!)
342
+ query = Users.update({'email': 'newemail@example.com'})
343
+ sql, params = query.render()
344
+ # ('UPDATE users SET email = ?', ['newemail@example.com'])
345
+
346
+ # UPDATE with WHERE
347
+ query = Users.update({'email': 'newemail@example.com'}).where(Users.id == 'abc123')
348
+ sql, params = query.render()
349
+ # ('UPDATE users SET email = ? WHERE users.id = ?', ['newemail@example.com', 'abc123'])
350
+
351
+ # Multiple WHERE conditions
352
+ query = (Users.update({'email': 'newemail@example.com'})
353
+ .where(Users.id == 'abc123')
354
+ .where(Users.age > 18))
355
+
356
+ # With RETURNING (Postgres/SQLite)
357
+ query = (Users.update({'email': 'new@example.com'})
358
+ .where(Users.id == 'abc123')
359
+ .returning())
360
+ # ('UPDATE users SET email = ? WHERE users.id = ? RETURNING *', [...])
361
+ ```
362
+
363
+ ### DELETE
364
+
365
+ ```python
366
+ # Basic delete (no WHERE = deletes all rows!)
367
+ query = Users.delete()
368
+ sql, params = query.render()
369
+ # ('DELETE FROM users', [])
370
+
371
+ # DELETE with WHERE
372
+ query = Users.delete().where(Users.id == 'abc123')
373
+ sql, params = query.render()
374
+ # ('DELETE FROM users WHERE users.id = ?', ['abc123'])
375
+
376
+ # Multiple conditions
377
+ query = Users.delete().where(Users.age < 18).where(Users.active == False)
378
+
379
+ # With RETURNING (Postgres/SQLite)
380
+ query = Users.delete().where(Users.id == 'abc123').returning()
381
+ # ('DELETE FROM users WHERE users.id = ? RETURNING *', ['abc123'])
382
+ ```
383
+
384
+ ## Database Compatibility
385
+
386
+ The query builder is database-agnostic - all methods are available regardless of which database you're using. It's your responsibility to use the appropriate methods for your database:
387
+
388
+ **PostgreSQL:**
389
+ - ✅ `.returning()` - RETURNING clause
390
+ - ✅ `.on_conflict_do_nothing()` - ON CONFLICT DO NOTHING
391
+ - ✅ `.on_conflict_update()` - ON CONFLICT DO UPDATE with EXCLUDED.*
392
+ - ❌ `.ignore()` - Not supported
393
+ - ❌ `.on_duplicate_key_update()` - Not supported
394
+
395
+ **MySQL:**
396
+ - ❌ `.returning()` - Not supported (MySQL limitation)
397
+ - ✅ `.ignore()` - INSERT IGNORE
398
+ - ✅ `.on_duplicate_key_update()` - ON DUPLICATE KEY UPDATE with VALUES()
399
+ - ❌ `.on_conflict_do_nothing()` - Not supported
400
+ - ❌ `.on_conflict_update()` - Not supported
401
+
402
+ **SQLite:**
403
+ - ✅ `.returning()` - RETURNING clause (SQLite 3.35+)
404
+ - ✅ `.on_conflict_do_nothing()` - ON CONFLICT DO NOTHING
405
+ - ✅ `.on_conflict_update()` - ON CONFLICT DO UPDATE
406
+ - ❌ `.ignore()` - Not supported
407
+ - ❌ `.on_duplicate_key_update()` - Not supported
408
+
409
+ If you use an unsupported method, your database will raise a syntax error when you execute the query.
410
+
411
+ ## Mixing Query Builder with T-Strings
412
+
413
+ You can combine the query builder with raw t-strings for complex logic:
414
+
415
+ ```python
416
+ from tsql.query_builder import Table, Column
417
+
418
+ class Users(Table):
419
+ id: Column
420
+ name: Column
421
+ age: Column
422
+ email: Column
423
+
424
+ # Start with query builder
425
+ query = Users.select(Users.id, Users.name, Users.email)
426
+
427
+ # Add structured condition
428
+ query = query.where(Users.age > 18)
429
+
430
+ # Add complex t-string condition for OR logic
431
+ search_term = "john"
432
+ name_col = str(Users.name)
433
+ email_col = str(Users.email)
434
+ complex_condition = t"{name_col:literal} LIKE '%' || {search_term} || '%' OR {email_col:literal} LIKE '%' || {search_term} || '%'"
435
+ query = query.where(complex_condition)
436
+
437
+ sql, params = query.render()
438
+ # SELECT users.id, users.name, users.email FROM users
439
+ # WHERE users.age > ? AND (users.name LIKE '%' || ? || '%' OR users.email LIKE '%' || ? || '%')
440
+ # params: [18, 'john', 'john']
441
+ ```
442
+
443
+ Note: T-string conditions passed to `.where()` are automatically wrapped in parentheses to ensure proper operator precedence.
444
+
445
+ ## SQLAlchemy & Alembic Integration
446
+
447
+ The query builder can integrate with SQLAlchemy's metadata system for alembic autogenerate:
448
+
449
+ ```bash
450
+ pip install t-sql[sqlalchemy]
451
+ # or
452
+ uv add t-sql --optional sqlalchemy
453
+ ```
454
+
455
+ ### Two Ways to Define Columns
456
+
457
+ **1. Simple Column annotations** (for query builder only):
458
+
459
+ ```python
460
+ from tsql import Table, Column
461
+
462
+ class Users(Table):
463
+ id: Column
464
+ name: Column
465
+ age: Column
466
+ ```
467
+
468
+ **2. SQLAlchemy Column objects** (for alembic integration):
469
+
470
+ ```python
471
+ from sqlalchemy import MetaData, Column, String, Integer, ForeignKey
472
+ from tsql.query_builder import Table
473
+
474
+ metadata = MetaData()
475
+
476
+ class Users(Table, metadata=metadata):
477
+ id = Column(String, primary_key=True)
478
+ email = Column(String(255), unique=True, nullable=False)
479
+ name = Column(String(100))
480
+ age = Column(Integer)
481
+
482
+ # Use for alembic
483
+ target_metadata = metadata
484
+
485
+ # Use for queries (works identically!)
486
+ query = Users.select().where(Users.age > 18)
487
+ ```
488
+
489
+ You can mix both approaches:
490
+
491
+ ```python
492
+ from sqlalchemy import Column, String, DateTime
493
+ from sqlalchemy.sql.functions import now
494
+
495
+ class Events(Table, metadata=metadata):
496
+ id = Column(String, primary_key=True)
497
+ topic: Column # Simple annotation - becomes nullable String column
498
+ created_at = Column(DateTime, server_default=now())
499
+ ```
500
+
501
+ ## Schema Support
502
+
503
+ ```python
504
+ class Users(Table, schema='public'):
505
+ id: Column
506
+ name: Column
507
+ ```
508
+
509
+ Or with custom table name and schema:
510
+
511
+ ```python
512
+ class Users(Table, table_name='user_accounts', schema='public'):
513
+ id: Column
514
+ name: Column
515
+ ```
516
+
517
+ # Note on Usage
518
+
519
+ This library should ideally be used in middleware or library code right before making a query. It can enforce the use of t-strings and prevent raw strings:
520
+
521
+ ```python
522
+ from string.templatelib import Template
523
+ import tsql
524
+
525
+ def execute_sql_query(query):
526
+ if not isinstance(query, Template):
527
+ raise TypeError('Cannot make a query without using t-strings')
528
+
529
+ return sql_engine.execute(*tsql.render(query))
530
+ ```