t-sql 3.0.0__tar.gz → 3.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.
Files changed (31) hide show
  1. {t_sql-3.0.0 → t_sql-3.1.0}/PKG-INFO +19 -25
  2. {t_sql-3.0.0 → t_sql-3.1.0}/README.md +18 -24
  3. {t_sql-3.0.0 → t_sql-3.1.0}/pyproject.toml +1 -1
  4. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_query_builder.py +36 -4
  5. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_sqlite_integration.py +1 -1
  6. {t_sql-3.0.0 → t_sql-3.1.0}/tsql/query_builder.py +48 -4
  7. {t_sql-3.0.0 → t_sql-3.1.0}/.dockerignore +0 -0
  8. {t_sql-3.0.0 → t_sql-3.1.0}/.github/workflows/publish.yml +0 -0
  9. {t_sql-3.0.0 → t_sql-3.1.0}/.github/workflows/test.yml +0 -0
  10. {t_sql-3.0.0 → t_sql-3.1.0}/.gitignore +0 -0
  11. {t_sql-3.0.0 → t_sql-3.1.0}/Dockerfile +0 -0
  12. {t_sql-3.0.0 → t_sql-3.1.0}/LICENSE +0 -0
  13. {t_sql-3.0.0 → t_sql-3.1.0}/compose.yaml +0 -0
  14. {t_sql-3.0.0 → t_sql-3.1.0}/context7.json +0 -0
  15. {t_sql-3.0.0 → t_sql-3.1.0}/pytest.ini +0 -0
  16. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_alembic_integration.py +0 -0
  17. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_asyncpg_integration.py +0 -0
  18. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_different_object_types.py +0 -0
  19. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_escaped.py +0 -0
  20. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_escaped_binary_hex.py +0 -0
  21. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_helper_functions.py +0 -0
  22. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_injection_edge_cases.py +0 -0
  23. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_injection_protection_validation.py +0 -0
  24. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_injections_for_escaped.py +0 -0
  25. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_mysql_integration.py +0 -0
  26. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_parameter_names.py +0 -0
  27. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_sqlalchemy_integration.py +0 -0
  28. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_styles.py +0 -0
  29. {t_sql-3.0.0 → t_sql-3.1.0}/tests/test_tsql.py +0 -0
  30. {t_sql-3.0.0 → t_sql-3.1.0}/tsql/__init__.py +0 -0
  31. {t_sql-3.0.0 → t_sql-3.1.0}/tsql/styles.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t-sql
3
- Version: 3.0.0
3
+ Version: 3.1.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
@@ -159,8 +159,7 @@ sql, params = query.render()
159
159
  Quick INSERT queries:
160
160
 
161
161
  ```python
162
- values = {'id': 'abc123', 'name': 'bob', 'email': 'bob@example.com'}
163
- query = tsql.insert('users', values)
162
+ query = tsql.insert('users', id='abc123', name='bob', email='bob@example.com')
164
163
  sql, params = query.render()
165
164
  # ('INSERT INTO users (id, name, email) VALUES (?, ?, ?)', ['abc123', 'bob', 'bob@example.com'])
166
165
  ```
@@ -171,14 +170,9 @@ Quick UPDATE queries:
171
170
 
172
171
  ```python
173
172
  # Update by ID
174
- query = tsql.update('users', {'email': 'new@example.com'}, id_value='abc123')
173
+ query = tsql.update('users', 'abc123', email='new@example.com')
175
174
  sql, params = query.render()
176
175
  # ('UPDATE users SET email = ? WHERE id = ?', ['new@example.com', 'abc123'])
177
-
178
- # Update with custom WHERE
179
- query = tsql.update('users', {'email': 'new@example.com'}, where={'age': 25})
180
- sql, params = query.render()
181
- # ('UPDATE users SET email = ? WHERE age = ?', ['new@example.com', 25])
182
176
  ```
183
177
 
184
178
  #### delete
@@ -271,8 +265,9 @@ query = Users.select().where(Users.id.in_([1, 2, 3]))
271
265
  query = Users.select().where(Users.username.like('%john%'))
272
266
 
273
267
  # ORDER BY
274
- query = Posts.select().order_by(Posts.id)
275
- query = Posts.select().order_by((Posts.id, 'DESC'))
268
+ query = Posts.select().order_by(Posts.id) # defaults to ASC
269
+ query = Posts.select().order_by(Posts.id.desc())
270
+ query = Posts.select().order_by(Posts.created_at.asc(), Posts.id.desc())
276
271
 
277
272
  # LIMIT and OFFSET
278
273
  query = Posts.select().limit(10).offset(20)
@@ -291,47 +286,46 @@ The query builder supports INSERT, UPDATE, and DELETE with database-agnostic con
291
286
 
292
287
  ```python
293
288
  # Basic insert
294
- values = {'id': 'abc123', 'username': 'john', 'email': 'john@example.com'}
295
- query = Users.insert(values)
289
+ query = Users.insert(id='abc123', username='john', email='john@example.com')
296
290
  sql, params = query.render()
297
291
  # ('INSERT INTO users (id, username, email) VALUES (?, ?, ?)', ['abc123', 'john', 'john@example.com'])
298
292
 
299
293
  # INSERT with RETURNING (Postgres/SQLite)
300
- query = Users.insert(values).returning()
294
+ query = Users.insert(id='abc123', username='john', email='john@example.com').returning()
301
295
  sql, params = query.render()
302
296
  # ('INSERT INTO users (id, username, email) VALUES (?, ?, ?) RETURNING *', [...])
303
297
 
304
298
  # INSERT IGNORE (MySQL)
305
- query = Users.insert(values).ignore()
299
+ query = Users.insert(id='abc123', username='john', email='john@example.com').ignore()
306
300
  sql, params = query.render()
307
301
  # ('INSERT IGNORE INTO users (id, username, email) VALUES (?, ?, ?)', [...])
308
302
 
309
303
  # ON CONFLICT DO NOTHING (Postgres/SQLite)
310
- query = Users.insert(values).on_conflict_do_nothing()
304
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_conflict_do_nothing()
311
305
  # ('INSERT INTO users (...) VALUES (...) ON CONFLICT DO NOTHING', [...])
312
306
 
313
307
  # ON CONFLICT DO NOTHING with specific conflict target (Postgres/SQLite)
314
- query = Users.insert(values).on_conflict_do_nothing(conflict_on='email')
308
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_conflict_do_nothing(conflict_on='email')
315
309
  # ('INSERT INTO users (...) VALUES (...) ON CONFLICT (email) DO NOTHING', [...])
316
310
 
317
311
  # ON CONFLICT DO UPDATE (Postgres/SQLite upsert)
318
- query = Users.insert(values).on_conflict_update(conflict_on='id')
312
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_conflict_update(conflict_on='id')
319
313
  # ('INSERT INTO users (...) VALUES (...)
320
314
  # ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, email = EXCLUDED.email', [...])
321
315
 
322
316
  # ON CONFLICT with custom update
323
- query = Users.insert(values).on_conflict_update(
317
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_conflict_update(
324
318
  conflict_on='id',
325
319
  update={'username': 'updated_name'}
326
320
  )
327
321
 
328
322
  # ON DUPLICATE KEY UPDATE (MySQL)
329
- query = Users.insert(values).on_duplicate_key_update()
323
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_duplicate_key_update()
330
324
  # ('INSERT INTO users (...) VALUES (...)
331
325
  # ON DUPLICATE KEY UPDATE id = VALUES(id), username = VALUES(username), ...', [...])
332
326
 
333
327
  # Chain multiple modifiers
334
- query = (Users.insert(values)
328
+ query = (Users.insert(id='abc123', username='john', email='john@example.com')
335
329
  .on_conflict_update(conflict_on='id')
336
330
  .returning('id', 'username'))
337
331
  ```
@@ -340,22 +334,22 @@ query = (Users.insert(values)
340
334
 
341
335
  ```python
342
336
  # Basic update (no WHERE = updates all rows!)
343
- query = Users.update({'email': 'newemail@example.com'})
337
+ query = Users.update(email='newemail@example.com')
344
338
  sql, params = query.render()
345
339
  # ('UPDATE users SET email = ?', ['newemail@example.com'])
346
340
 
347
341
  # UPDATE with WHERE
348
- query = Users.update({'email': 'newemail@example.com'}).where(Users.id == 'abc123')
342
+ query = Users.update(email='newemail@example.com').where(Users.id == 'abc123')
349
343
  sql, params = query.render()
350
344
  # ('UPDATE users SET email = ? WHERE users.id = ?', ['newemail@example.com', 'abc123'])
351
345
 
352
346
  # Multiple WHERE conditions
353
- query = (Users.update({'email': 'newemail@example.com'})
347
+ query = (Users.update(email='newemail@example.com')
354
348
  .where(Users.id == 'abc123')
355
349
  .where(Users.age > 18))
356
350
 
357
351
  # With RETURNING (Postgres/SQLite)
358
- query = (Users.update({'email': 'new@example.com'})
352
+ query = (Users.update(email='new@example.com')
359
353
  .where(Users.id == 'abc123')
360
354
  .returning())
361
355
  # ('UPDATE users SET email = ? WHERE users.id = ? RETURNING *', [...])
@@ -149,8 +149,7 @@ sql, params = query.render()
149
149
  Quick INSERT queries:
150
150
 
151
151
  ```python
152
- values = {'id': 'abc123', 'name': 'bob', 'email': 'bob@example.com'}
153
- query = tsql.insert('users', values)
152
+ query = tsql.insert('users', id='abc123', name='bob', email='bob@example.com')
154
153
  sql, params = query.render()
155
154
  # ('INSERT INTO users (id, name, email) VALUES (?, ?, ?)', ['abc123', 'bob', 'bob@example.com'])
156
155
  ```
@@ -161,14 +160,9 @@ Quick UPDATE queries:
161
160
 
162
161
  ```python
163
162
  # Update by ID
164
- query = tsql.update('users', {'email': 'new@example.com'}, id_value='abc123')
163
+ query = tsql.update('users', 'abc123', email='new@example.com')
165
164
  sql, params = query.render()
166
165
  # ('UPDATE users SET email = ? WHERE id = ?', ['new@example.com', 'abc123'])
167
-
168
- # Update with custom WHERE
169
- query = tsql.update('users', {'email': 'new@example.com'}, where={'age': 25})
170
- sql, params = query.render()
171
- # ('UPDATE users SET email = ? WHERE age = ?', ['new@example.com', 25])
172
166
  ```
173
167
 
174
168
  #### delete
@@ -261,8 +255,9 @@ query = Users.select().where(Users.id.in_([1, 2, 3]))
261
255
  query = Users.select().where(Users.username.like('%john%'))
262
256
 
263
257
  # ORDER BY
264
- query = Posts.select().order_by(Posts.id)
265
- query = Posts.select().order_by((Posts.id, 'DESC'))
258
+ query = Posts.select().order_by(Posts.id) # defaults to ASC
259
+ query = Posts.select().order_by(Posts.id.desc())
260
+ query = Posts.select().order_by(Posts.created_at.asc(), Posts.id.desc())
266
261
 
267
262
  # LIMIT and OFFSET
268
263
  query = Posts.select().limit(10).offset(20)
@@ -281,47 +276,46 @@ The query builder supports INSERT, UPDATE, and DELETE with database-agnostic con
281
276
 
282
277
  ```python
283
278
  # Basic insert
284
- values = {'id': 'abc123', 'username': 'john', 'email': 'john@example.com'}
285
- query = Users.insert(values)
279
+ query = Users.insert(id='abc123', username='john', email='john@example.com')
286
280
  sql, params = query.render()
287
281
  # ('INSERT INTO users (id, username, email) VALUES (?, ?, ?)', ['abc123', 'john', 'john@example.com'])
288
282
 
289
283
  # INSERT with RETURNING (Postgres/SQLite)
290
- query = Users.insert(values).returning()
284
+ query = Users.insert(id='abc123', username='john', email='john@example.com').returning()
291
285
  sql, params = query.render()
292
286
  # ('INSERT INTO users (id, username, email) VALUES (?, ?, ?) RETURNING *', [...])
293
287
 
294
288
  # INSERT IGNORE (MySQL)
295
- query = Users.insert(values).ignore()
289
+ query = Users.insert(id='abc123', username='john', email='john@example.com').ignore()
296
290
  sql, params = query.render()
297
291
  # ('INSERT IGNORE INTO users (id, username, email) VALUES (?, ?, ?)', [...])
298
292
 
299
293
  # ON CONFLICT DO NOTHING (Postgres/SQLite)
300
- query = Users.insert(values).on_conflict_do_nothing()
294
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_conflict_do_nothing()
301
295
  # ('INSERT INTO users (...) VALUES (...) ON CONFLICT DO NOTHING', [...])
302
296
 
303
297
  # ON CONFLICT DO NOTHING with specific conflict target (Postgres/SQLite)
304
- query = Users.insert(values).on_conflict_do_nothing(conflict_on='email')
298
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_conflict_do_nothing(conflict_on='email')
305
299
  # ('INSERT INTO users (...) VALUES (...) ON CONFLICT (email) DO NOTHING', [...])
306
300
 
307
301
  # ON CONFLICT DO UPDATE (Postgres/SQLite upsert)
308
- query = Users.insert(values).on_conflict_update(conflict_on='id')
302
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_conflict_update(conflict_on='id')
309
303
  # ('INSERT INTO users (...) VALUES (...)
310
304
  # ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, email = EXCLUDED.email', [...])
311
305
 
312
306
  # ON CONFLICT with custom update
313
- query = Users.insert(values).on_conflict_update(
307
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_conflict_update(
314
308
  conflict_on='id',
315
309
  update={'username': 'updated_name'}
316
310
  )
317
311
 
318
312
  # ON DUPLICATE KEY UPDATE (MySQL)
319
- query = Users.insert(values).on_duplicate_key_update()
313
+ query = Users.insert(id='abc123', username='john', email='john@example.com').on_duplicate_key_update()
320
314
  # ('INSERT INTO users (...) VALUES (...)
321
315
  # ON DUPLICATE KEY UPDATE id = VALUES(id), username = VALUES(username), ...', [...])
322
316
 
323
317
  # Chain multiple modifiers
324
- query = (Users.insert(values)
318
+ query = (Users.insert(id='abc123', username='john', email='john@example.com')
325
319
  .on_conflict_update(conflict_on='id')
326
320
  .returning('id', 'username'))
327
321
  ```
@@ -330,22 +324,22 @@ query = (Users.insert(values)
330
324
 
331
325
  ```python
332
326
  # Basic update (no WHERE = updates all rows!)
333
- query = Users.update({'email': 'newemail@example.com'})
327
+ query = Users.update(email='newemail@example.com')
334
328
  sql, params = query.render()
335
329
  # ('UPDATE users SET email = ?', ['newemail@example.com'])
336
330
 
337
331
  # UPDATE with WHERE
338
- query = Users.update({'email': 'newemail@example.com'}).where(Users.id == 'abc123')
332
+ query = Users.update(email='newemail@example.com').where(Users.id == 'abc123')
339
333
  sql, params = query.render()
340
334
  # ('UPDATE users SET email = ? WHERE users.id = ?', ['newemail@example.com', 'abc123'])
341
335
 
342
336
  # Multiple WHERE conditions
343
- query = (Users.update({'email': 'newemail@example.com'})
337
+ query = (Users.update(email='newemail@example.com')
344
338
  .where(Users.id == 'abc123')
345
339
  .where(Users.age > 18))
346
340
 
347
341
  # With RETURNING (Postgres/SQLite)
348
- query = (Users.update({'email': 'new@example.com'})
342
+ query = (Users.update(email='new@example.com')
349
343
  .where(Users.id == 'abc123')
350
344
  .returning())
351
345
  # ('UPDATE users SET email = ? WHERE users.id = ? RETURNING *', [...])
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "t-sql"
7
- version = "3.0.0"
7
+ version = "3.1.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"
@@ -213,7 +213,7 @@ def test_order_by():
213
213
 
214
214
  def test_order_by_desc():
215
215
  """Test ORDER BY with DESC"""
216
- query = Users.select().order_by((Users.id, 'DESC'))
216
+ query = Users.select().order_by(Users.id.desc())
217
217
  sql, params = query.render()
218
218
 
219
219
  assert 'ORDER BY users.id DESC' in sql
@@ -221,7 +221,39 @@ def test_order_by_desc():
221
221
 
222
222
  def test_order_by_multiple():
223
223
  """Test ORDER BY with multiple columns"""
224
- query = Users.select().order_by(Users.username, (Users.id, 'DESC'))
224
+ query = Users.select().order_by(Users.username, Users.id.desc())
225
+ sql, params = query.render()
226
+
227
+ assert 'ORDER BY users.username ASC, users.id DESC' in sql
228
+
229
+
230
+ def test_order_by_asc_method():
231
+ """Test ORDER BY with .asc() method"""
232
+ query = Users.select().order_by(Users.username.asc())
233
+ sql, params = query.render()
234
+
235
+ assert 'ORDER BY users.username ASC' in sql
236
+
237
+
238
+ def test_order_by_desc_method():
239
+ """Test ORDER BY with .desc() method"""
240
+ query = Users.select().order_by(Users.id.desc())
241
+ sql, params = query.render()
242
+
243
+ assert 'ORDER BY users.id DESC' in sql
244
+
245
+
246
+ def test_order_by_mixed_methods():
247
+ """Test ORDER BY with mixed .asc() and .desc() methods"""
248
+ query = Users.select().order_by(Users.username.asc(), Users.id.desc())
249
+ sql, params = query.render()
250
+
251
+ assert 'ORDER BY users.username ASC, users.id DESC' in sql
252
+
253
+
254
+ def test_order_by_mixed_syntax():
255
+ """Test ORDER BY with mixed method calls and bare columns"""
256
+ query = Users.select().order_by(Users.username, Users.id.desc())
225
257
  sql, params = query.render()
226
258
 
227
259
  assert 'ORDER BY users.username ASC, users.id DESC' in sql
@@ -242,7 +274,7 @@ def test_complex_query():
242
274
  .join(Users, Posts.user_id == Users.id)
243
275
  .where(Posts.id > 100)
244
276
  .where(Users.id >= 5)
245
- .order_by((Posts.id, 'DESC'))
277
+ .order_by(Posts.id.desc())
246
278
  .limit(20))
247
279
  sql, params = query.render()
248
280
 
@@ -480,7 +512,7 @@ def test_complex_aggregation_query():
480
512
  .where(Posts.id > 100)
481
513
  .group_by(Posts.user_id)
482
514
  .having(Posts.id > 5)
483
- .order_by((Posts.user_id, 'DESC'))
515
+ .order_by(Posts.user_id.desc())
484
516
  .limit(10)
485
517
  .offset(5))
486
518
  sql, params = query.render()
@@ -331,7 +331,7 @@ async def test_query_builder_select(conn):
331
331
  assert rows[0][0] == 'Alice'
332
332
 
333
333
  # Test ORDER BY
334
- query = TestUsers.select().order_by((TestUsers.age, 'DESC'))
334
+ query = TestUsers.select().order_by(TestUsers.age.desc())
335
335
  sql, params = query.render()
336
336
 
337
337
  cursor = await conn.execute(sql, params)
@@ -16,6 +16,17 @@ except ImportError:
16
16
  SAColumnType = None
17
17
 
18
18
 
19
+ class OrderByClause:
20
+ """Represents a column with an ORDER BY direction (ASC/DESC)"""
21
+
22
+ def __init__(self, column: 'Column', direction: str):
23
+ self.column = column
24
+ self.direction = direction.upper()
25
+
26
+ def __repr__(self) -> str:
27
+ return f"OrderByClause({self.column!r}, {self.direction!r})"
28
+
29
+
19
30
  class Column:
20
31
  """Represents a bound column (table + column name) for building queries"""
21
32
 
@@ -140,6 +151,28 @@ class Column:
140
151
  """Create an IS NOT NULL condition"""
141
152
  return Condition(self, 'IS NOT', None)
142
153
 
154
+ def asc(self) -> OrderByClause:
155
+ """Create an ascending ORDER BY clause
156
+
157
+ Returns:
158
+ OrderByClause for use in order_by()
159
+
160
+ Example:
161
+ Users.select().order_by(Users.username.asc())
162
+ """
163
+ return OrderByClause(self, 'ASC')
164
+
165
+ def desc(self) -> OrderByClause:
166
+ """Create a descending ORDER BY clause
167
+
168
+ Returns:
169
+ OrderByClause for use in order_by()
170
+
171
+ Example:
172
+ Users.select().order_by(Users.created_at.desc())
173
+ """
174
+ return OrderByClause(self, 'DESC')
175
+
143
176
 
144
177
  class Table:
145
178
  """Base class for all table definitions. Provides query builder methods.
@@ -838,11 +871,22 @@ class SelectQueryBuilder(QueryBuilder):
838
871
  """Add a RIGHT JOIN clause"""
839
872
  return self.join(table, on, 'RIGHT')
840
873
 
841
- def order_by(self, *columns: Union[Column, tuple[Column, str]]) -> 'SelectQueryBuilder':
842
- """Add ORDER BY clause. Pass (column, 'DESC') for descending"""
874
+ def order_by(self, *columns: Union[Column, OrderByClause]) -> 'SelectQueryBuilder':
875
+ """Add ORDER BY clause
876
+
877
+ Args:
878
+ columns: Column objects or OrderByClause objects (from .asc()/.desc())
879
+
880
+ Examples:
881
+ # Using .asc() and .desc() methods
882
+ Users.select().order_by(Users.username.asc(), Users.id.desc())
883
+
884
+ # Bare column defaults to ASC
885
+ Users.select().order_by(Users.username)
886
+ """
843
887
  for col in columns:
844
- if isinstance(col, tuple):
845
- self._order_by_columns.append(col)
888
+ if isinstance(col, OrderByClause):
889
+ self._order_by_columns.append((col.column, col.direction))
846
890
  else:
847
891
  self._order_by_columns.append((col, 'ASC'))
848
892
  return self
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