t-sql 4.4.0__tar.gz → 4.4.2__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 (33) hide show
  1. {t_sql-4.4.0 → t_sql-4.4.2}/PKG-INFO +1 -1
  2. {t_sql-4.4.0 → t_sql-4.4.2}/pyproject.toml +1 -1
  3. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_type_processor.py +58 -0
  4. {t_sql-4.4.0 → t_sql-4.4.2}/tsql/query_builder.py +30 -18
  5. {t_sql-4.4.0 → t_sql-4.4.2}/.dockerignore +0 -0
  6. {t_sql-4.4.0 → t_sql-4.4.2}/.github/workflows/publish.yml +0 -0
  7. {t_sql-4.4.0 → t_sql-4.4.2}/.github/workflows/test.yml +0 -0
  8. {t_sql-4.4.0 → t_sql-4.4.2}/.gitignore +0 -0
  9. {t_sql-4.4.0 → t_sql-4.4.2}/Dockerfile +0 -0
  10. {t_sql-4.4.0 → t_sql-4.4.2}/LICENSE +0 -0
  11. {t_sql-4.4.0 → t_sql-4.4.2}/README.md +0 -0
  12. {t_sql-4.4.0 → t_sql-4.4.2}/compose.yaml +0 -0
  13. {t_sql-4.4.0 → t_sql-4.4.2}/context7.json +0 -0
  14. {t_sql-4.4.0 → t_sql-4.4.2}/pytest.ini +0 -0
  15. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_alembic_integration.py +0 -0
  16. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_asyncpg_integration.py +0 -0
  17. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_different_object_types.py +0 -0
  18. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_escaped.py +0 -0
  19. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_escaped_binary_hex.py +0 -0
  20. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_helper_functions.py +0 -0
  21. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_injection_edge_cases.py +0 -0
  22. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_injection_protection_validation.py +0 -0
  23. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_injections_for_escaped.py +0 -0
  24. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_mysql_integration.py +0 -0
  25. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_parameter_names.py +0 -0
  26. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_query_builder.py +0 -0
  27. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_sqlalchemy_integration.py +0 -0
  28. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_sqlite_integration.py +0 -0
  29. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_styles.py +0 -0
  30. {t_sql-4.4.0 → t_sql-4.4.2}/tests/test_tsql.py +0 -0
  31. {t_sql-4.4.0 → t_sql-4.4.2}/tsql/__init__.py +0 -0
  32. {t_sql-4.4.0 → t_sql-4.4.2}/tsql/styles.py +0 -0
  33. {t_sql-4.4.0 → t_sql-4.4.2}/tsql/type_processor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t-sql
3
- Version: 4.4.0
3
+ Version: 4.4.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "t-sql"
7
- version = "4.4.0"
7
+ version = "4.4.2"
8
8
  description = "Safe SQL. SQL queries for python t-strings (PEP 750)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -258,3 +258,61 @@ def test_type_processor_not_applied_to_column_comparisons():
258
258
  # No parameters should be generated (column-to-column comparison)
259
259
  assert len(params) == 0
260
260
  assert "user.ssn = user.backup_ssn" in sql
261
+
262
+
263
+ @pytest.mark.skipif(not HAS_SQLALCHEMY, reason="SQLAlchemy not installed")
264
+ def test_map_results_with_joins_and_aliases():
265
+ """Test that map_results handles JOINs and column aliases correctly"""
266
+ metadata = MetaData()
267
+
268
+ class User(Table, metadata=metadata):
269
+ id = SAColumn(Integer, primary_key=True)
270
+ name = SAColumn(String(100))
271
+ ssn = SAColumn(String(255), type_processor=EncryptedString(key="secret123"))
272
+
273
+ class Profile(Table, metadata=metadata):
274
+ id = SAColumn(Integer, primary_key=True)
275
+ user_id = SAColumn(Integer)
276
+ metadata_col = SAColumn(String(255), type_processor=JSONType())
277
+
278
+ # Test with explicit columns including alias
279
+ query = (
280
+ User.select(User.id, User.name, User.ssn.as_('social'), Profile.metadata_col)
281
+ .join(Profile, on=User.id == Profile.user_id)
282
+ )
283
+
284
+ # Simulate database rows (using the format that EncryptedString produces)
285
+ rows = [
286
+ {
287
+ "id": 1,
288
+ "name": "Alice",
289
+ "social": "encrypted_123-45-6789_secret123", # Aliased column
290
+ "metadata_col": '{"key": "value"}'
291
+ }
292
+ ]
293
+
294
+ query.map_results(rows)
295
+
296
+ # Check aliased column was decrypted
297
+ assert rows[0]["social"] == "123-45-6789"
298
+ # Check joined table column was deserialized
299
+ assert rows[0]["metadata_col"] == {"key": "value"}
300
+
301
+ # Test with SELECT * (should process all columns from all tables)
302
+ query_star = User.select().join(Profile, on=User.id == Profile.user_id)
303
+
304
+ rows_star = [
305
+ {
306
+ "id": 1,
307
+ "name": "Alice",
308
+ "ssn": "encrypted_987-65-4321_secret123",
309
+ "user_id": 1,
310
+ "metadata_col": '{"foo": "bar"}'
311
+ }
312
+ ]
313
+
314
+ query_star.map_results(rows_star)
315
+
316
+ # Both processors should be applied
317
+ assert rows_star[0]["ssn"] == "987-65-4321"
318
+ assert rows_star[0]["metadata_col"] == {"foo": "bar"}
@@ -566,37 +566,49 @@ class QueryBuilder(ABC):
566
566
  database values back to Python values (e.g., decrypt encrypted fields,
567
567
  deserialize JSON, etc.).
568
568
 
569
+ Modifies row objects in place to preserve driver-specific row types
570
+ (e.g., asyncpg Record objects).
571
+
569
572
  Args:
570
- rows: List of dictionaries from database query results
573
+ rows: List of row objects from database query results
571
574
 
572
575
  Returns:
573
- List of dictionaries with transformed values
576
+ The same list of rows with transformed values (mutated in place)
574
577
 
575
578
  Example:
576
579
  query = User.select().where(User.id == 1)
577
580
  rows = await conn.fetch(*query.render())
578
- transformed_rows = query.map_results(rows) # ssn decrypted, metadata deserialized
581
+ query.map_results(rows) # ssn decrypted, metadata deserialized (in place)
579
582
  """
580
583
  if not hasattr(self, 'base_table'):
581
584
  raise AttributeError("map_results requires a base_table attribute")
582
585
 
583
- results = []
584
- for row in rows:
585
- # Convert row to dict if it's not already (some drivers return Record objects)
586
- row_dict = dict(row) if not isinstance(row, dict) else row
587
-
588
- # Apply type processors
589
- transformed = {}
590
- for col_name, value in row_dict.items():
591
- if col_name in self.base_table._type_processors:
592
- processor = self.base_table._type_processors[col_name]
593
- transformed[col_name] = processor.process_result_value(value)
594
- else:
595
- transformed[col_name] = value
586
+ # Build a map of result_column_name -> processor
587
+ processors = {}
588
+
589
+ if hasattr(self, '_columns') and self._columns is not None:
590
+ # Explicit columns - we know exactly which ones and from which table
591
+ for col in self._columns:
592
+ if isinstance(col, Column) and col.type_processor:
593
+ # The result key is the alias if present, otherwise column_name
594
+ result_key = col.alias if col.alias else col.column_name
595
+ processors[result_key] = col.type_processor
596
+ else:
597
+ # SELECT * - check all tables involved (base table + joins)
598
+ tables = [self.base_table]
599
+ if hasattr(self, '_joins'):
600
+ tables.extend(join.table for join in self._joins)
596
601
 
597
- results.append(transformed)
602
+ for table in tables:
603
+ processors.update(table._type_processors)
604
+
605
+ # Apply processors to rows in place
606
+ for row in rows:
607
+ for col_name in list(row.keys()):
608
+ if col_name in processors:
609
+ row[col_name] = processors[col_name].process_result_value(row[col_name])
598
610
 
599
- return results
611
+ return rows
600
612
 
601
613
 
602
614
  class InsertBuilder(QueryBuilder):
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