t-sql 4.4.1__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.
- {t_sql-4.4.1 → t_sql-4.4.2}/PKG-INFO +1 -1
- {t_sql-4.4.1 → t_sql-4.4.2}/pyproject.toml +1 -1
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_type_processor.py +58 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tsql/query_builder.py +28 -16
- {t_sql-4.4.1 → t_sql-4.4.2}/.dockerignore +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/.github/workflows/publish.yml +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/.github/workflows/test.yml +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/.gitignore +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/Dockerfile +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/LICENSE +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/README.md +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/compose.yaml +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/context7.json +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/pytest.ini +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_alembic_integration.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_different_object_types.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_escaped.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_helper_functions.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_mysql_integration.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_parameter_names.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_query_builder.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_sqlite_integration.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_styles.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tests/test_tsql.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tsql/__init__.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tsql/styles.py +0 -0
- {t_sql-4.4.1 → t_sql-4.4.2}/tsql/type_processor.py +0 -0
|
@@ -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
|
-
|
|
570
|
-
|
|
569
|
+
Modifies row objects in place to preserve driver-specific row types
|
|
570
|
+
(e.g., asyncpg Record objects).
|
|
571
571
|
|
|
572
572
|
Args:
|
|
573
|
-
rows: List of
|
|
573
|
+
rows: List of row objects from database query results
|
|
574
574
|
|
|
575
575
|
Returns:
|
|
576
|
-
|
|
576
|
+
The same list of rows with transformed values (mutated in place)
|
|
577
577
|
|
|
578
578
|
Example:
|
|
579
579
|
query = User.select().where(User.id == 1)
|
|
580
580
|
rows = await conn.fetch(*query.render())
|
|
581
|
-
|
|
581
|
+
query.map_results(rows) # ssn decrypted, metadata deserialized (in place)
|
|
582
582
|
"""
|
|
583
583
|
if not hasattr(self, 'base_table'):
|
|
584
584
|
raise AttributeError("map_results requires a base_table attribute")
|
|
585
585
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
# Convert row to dict if it's not already (some drivers return Record objects)
|
|
589
|
-
row_dict = dict(row) if not isinstance(row, dict) else row
|
|
586
|
+
# Build a map of result_column_name -> processor
|
|
587
|
+
processors = {}
|
|
590
588
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|