t-sql 4.5.0__tar.gz → 4.5.1__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.5.0 → t_sql-4.5.1}/PKG-INFO +1 -1
- {t_sql-4.5.0 → t_sql-4.5.1}/pyproject.toml +1 -1
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_asyncpg_integration.py +35 -1
- t_sql-4.5.1/tests/test_template_in_builders.py +104 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_tsql.py +44 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tsql/__init__.py +31 -2
- {t_sql-4.5.0 → t_sql-4.5.1}/tsql/query_builder.py +32 -20
- {t_sql-4.5.0 → t_sql-4.5.1}/.dockerignore +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/.github/workflows/publish.yml +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/.github/workflows/test.yml +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/.gitignore +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/Dockerfile +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/LICENSE +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/README.md +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/compose.yaml +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/context7.json +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/pytest.ini +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_alembic_integration.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_different_object_types.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_escaped.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_helper_functions.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_mysql_integration.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_parameter_names.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_query_builder.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_sqlite_integration.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_styles.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tests/test_type_processor.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tsql/row.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tsql/styles.py +0 -0
- {t_sql-4.5.0 → t_sql-4.5.1}/tsql/type_processor.py +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncpg
|
|
2
|
+
import datetime
|
|
2
3
|
import os
|
|
3
4
|
import pytest
|
|
4
5
|
|
|
@@ -234,4 +235,37 @@ async def test_escaped_handles_comment_injection(conn):
|
|
|
234
235
|
malicious_input = "admin'--"
|
|
235
236
|
query, _ = tsql.render(t"SELECT * FROM test_users WHERE name = {malicious_input}", style=tsql.styles.ESCAPED)
|
|
236
237
|
rows = await conn.fetch(query)
|
|
237
|
-
assert len(rows) == 0
|
|
238
|
+
assert len(rows) == 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def test_datetime_comparison_with_asyncpg(conn):
|
|
242
|
+
"""Test that datetime objects work correctly with asyncpg (bug fix verification)"""
|
|
243
|
+
# Insert some test data with specific timestamps
|
|
244
|
+
base_time = datetime.datetime.now()
|
|
245
|
+
|
|
246
|
+
await conn.execute(
|
|
247
|
+
"INSERT INTO test_users (name, created_at) VALUES ($1, $2), ($3, $4), ($5, $6)",
|
|
248
|
+
'User1', base_time - datetime.timedelta(minutes=30),
|
|
249
|
+
'User2', base_time - datetime.timedelta(minutes=10),
|
|
250
|
+
'User3', base_time - datetime.timedelta(minutes=5)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Use a datetime object in a WHERE clause (this was broken before the fix)
|
|
254
|
+
cutoff_time = base_time - datetime.timedelta(minutes=15)
|
|
255
|
+
|
|
256
|
+
query, params = tsql.render(
|
|
257
|
+
t"SELECT name FROM test_users WHERE created_at > {cutoff_time}",
|
|
258
|
+
style=tsql.styles.NUMERIC_DOLLAR
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Verify the parameter is still a datetime object (not stringified)
|
|
262
|
+
assert len(params) == 1
|
|
263
|
+
assert isinstance(params[0], datetime.datetime)
|
|
264
|
+
|
|
265
|
+
# This should work without asyncpg throwing a DataError
|
|
266
|
+
rows = await conn.fetch(query, *params)
|
|
267
|
+
|
|
268
|
+
# Should find User2 and User3 (created within last 15 minutes)
|
|
269
|
+
assert len(rows) == 2
|
|
270
|
+
names = sorted([row['name'] for row in rows])
|
|
271
|
+
assert names == ['User2', 'User3']
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Tests for Template/TSQL values in INSERT/UPDATE builders"""
|
|
2
|
+
import pytest
|
|
3
|
+
from string.templatelib import Template
|
|
4
|
+
|
|
5
|
+
import tsql
|
|
6
|
+
from tsql import TSQL
|
|
7
|
+
from tsql.query_builder import Table, SAColumn
|
|
8
|
+
from tsql import styles
|
|
9
|
+
|
|
10
|
+
# Test tables
|
|
11
|
+
class TestTable(Table, table_name='test_table'):
|
|
12
|
+
id = ...
|
|
13
|
+
name = ...
|
|
14
|
+
created_ts = ...
|
|
15
|
+
updated_ts = ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_update_with_template_value():
|
|
19
|
+
"""Template values in UPDATE should be inlined, not parameterized"""
|
|
20
|
+
query = TestTable.update(updated_ts=t"NOW()").where(TestTable.id == 123)
|
|
21
|
+
|
|
22
|
+
sql, params = query.render(style=styles.QMARK)
|
|
23
|
+
|
|
24
|
+
# The Template should be inlined, not parameterized
|
|
25
|
+
assert "NOW()" in sql
|
|
26
|
+
assert sql == "UPDATE test_table SET updated_ts = NOW() WHERE test_table.id = ?"
|
|
27
|
+
# Only the id comparison should be parameterized
|
|
28
|
+
assert params == [123]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_insert_with_template_value():
|
|
32
|
+
"""Template values in INSERT should be inlined, not parameterized"""
|
|
33
|
+
query = TestTable.insert(name="Alice", created_ts=t"NOW()")
|
|
34
|
+
|
|
35
|
+
sql, params = query.render(style=styles.QMARK)
|
|
36
|
+
|
|
37
|
+
# The Template should be inlined, not parameterized
|
|
38
|
+
assert "NOW()" in sql
|
|
39
|
+
assert sql == "INSERT INTO test_table (name, created_ts) VALUES (?, NOW())"
|
|
40
|
+
# Only the name should be parameterized
|
|
41
|
+
assert params == ["Alice"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_insert_on_conflict_update_with_template():
|
|
45
|
+
"""Template values in ON CONFLICT UPDATE should be inlined"""
|
|
46
|
+
query = (TestTable.insert(id=1, name="Alice", updated_ts=t"NOW()")
|
|
47
|
+
.on_conflict_update('id', update={'updated_ts': t"NOW()"}))
|
|
48
|
+
|
|
49
|
+
sql, params = query.render(style=styles.QMARK)
|
|
50
|
+
|
|
51
|
+
# Both Templates should be inlined
|
|
52
|
+
assert sql.count("NOW()") == 2
|
|
53
|
+
assert "ON CONFLICT (id) DO UPDATE SET updated_ts = NOW()" in sql
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_insert_on_duplicate_key_with_template():
|
|
57
|
+
"""Template values in ON DUPLICATE KEY UPDATE should be inlined (MySQL)"""
|
|
58
|
+
query = (TestTable.insert(id=1, name="Alice", updated_ts=t"NOW()")
|
|
59
|
+
.on_duplicate_key_update(update={'updated_ts': t"NOW()"}))
|
|
60
|
+
|
|
61
|
+
sql, params = query.render(style=styles.QMARK)
|
|
62
|
+
|
|
63
|
+
# Both Templates should be inlined
|
|
64
|
+
assert sql.count("NOW()") == 2
|
|
65
|
+
assert "ON DUPLICATE KEY UPDATE updated_ts = NOW()" in sql
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_update_with_tsql_object():
|
|
69
|
+
"""TSQL objects in UPDATE should be inlined"""
|
|
70
|
+
now_expr = TSQL(t"NOW()")
|
|
71
|
+
query = TestTable.update(updated_ts=now_expr).where(TestTable.id == 123)
|
|
72
|
+
|
|
73
|
+
sql, params = query.render(style=styles.QMARK)
|
|
74
|
+
|
|
75
|
+
assert "NOW()" in sql
|
|
76
|
+
assert params == [123]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_update_multiple_template_values():
|
|
80
|
+
"""Multiple Template values should all be inlined"""
|
|
81
|
+
query = (TestTable.update(
|
|
82
|
+
created_ts=t"NOW()",
|
|
83
|
+
updated_ts=t"CURRENT_TIMESTAMP",
|
|
84
|
+
name="Bob"
|
|
85
|
+
).where(TestTable.id == 456))
|
|
86
|
+
|
|
87
|
+
sql, params = query.render(style=styles.QMARK)
|
|
88
|
+
|
|
89
|
+
# Both SQL expressions should be inlined
|
|
90
|
+
assert "NOW()" in sql
|
|
91
|
+
assert "CURRENT_TIMESTAMP" in sql
|
|
92
|
+
# Only name and id should be parameterized
|
|
93
|
+
assert params == ["Bob", 456]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_insert_with_sql_expression_postgres_style():
|
|
97
|
+
"""Template with SQL expression should work with different parameter styles"""
|
|
98
|
+
query = TestTable.insert(name="Charlie", created_ts=t"NOW() - INTERVAL '5 minutes'")
|
|
99
|
+
|
|
100
|
+
sql, params = query.render(style=styles.NUMERIC_DOLLAR)
|
|
101
|
+
|
|
102
|
+
assert "NOW() - INTERVAL '5 minutes'" in sql
|
|
103
|
+
assert sql == "INSERT INTO test_table (name, created_ts) VALUES ($1, NOW() - INTERVAL '5 minutes')"
|
|
104
|
+
assert params == ["Charlie"]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
import datetime
|
|
2
3
|
|
|
3
4
|
import tsql
|
|
4
5
|
import tsql.styles
|
|
@@ -111,6 +112,49 @@ def test_prevents_sql_injection():
|
|
|
111
112
|
assert result[0] == "SELECT * FROM table WHERE col=?"
|
|
112
113
|
|
|
113
114
|
|
|
115
|
+
def test_datetime_preserved_as_native_type():
|
|
116
|
+
"""datetime objects should be passed through unchanged, not stringified"""
|
|
117
|
+
dt = datetime.datetime(2025, 10, 21, 12, 30, 45, 123456, tzinfo=datetime.timezone.utc)
|
|
118
|
+
result = tsql.render(t"SELECT * FROM table WHERE created_at > {dt}")
|
|
119
|
+
assert result[0] == "SELECT * FROM table WHERE created_at > ?"
|
|
120
|
+
assert result[1] == [dt]
|
|
121
|
+
assert isinstance(result[1][0], datetime.datetime)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_date_preserved_as_native_type():
|
|
125
|
+
"""date objects should be passed through unchanged"""
|
|
126
|
+
d = datetime.date(2025, 10, 21)
|
|
127
|
+
result = tsql.render(t"SELECT * FROM table WHERE date_col = {d}")
|
|
128
|
+
assert result[0] == "SELECT * FROM table WHERE date_col = ?"
|
|
129
|
+
assert result[1] == [d]
|
|
130
|
+
assert isinstance(result[1][0], datetime.date)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_time_preserved_as_native_type():
|
|
134
|
+
"""time objects should be passed through unchanged"""
|
|
135
|
+
t = datetime.time(12, 30, 45)
|
|
136
|
+
result = tsql.render(t"SELECT * FROM table WHERE time_col = {t}")
|
|
137
|
+
assert result[0] == "SELECT * FROM table WHERE time_col = ?"
|
|
138
|
+
assert result[1] == [t]
|
|
139
|
+
assert isinstance(result[1][0], datetime.time)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_timedelta_preserved_as_native_type():
|
|
143
|
+
"""timedelta objects should be passed through unchanged"""
|
|
144
|
+
td = datetime.timedelta(days=15, hours=3, minutes=30)
|
|
145
|
+
result = tsql.render(t"SELECT * FROM table WHERE interval_col = {td}")
|
|
146
|
+
assert result[0] == "SELECT * FROM table WHERE interval_col = ?"
|
|
147
|
+
assert result[1] == [td]
|
|
148
|
+
assert isinstance(result[1][0], datetime.timedelta)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_datetime_with_format_spec_converts_to_string():
|
|
152
|
+
"""When a format spec is provided, datetime should be formatted as string"""
|
|
153
|
+
dt = datetime.datetime(2025, 10, 21, 12, 30, 45)
|
|
154
|
+
result = tsql.render(t"SELECT * FROM table WHERE date_str = {dt:%Y-%m-%d}")
|
|
155
|
+
assert result[0] == "SELECT * FROM table WHERE date_str = ?"
|
|
156
|
+
assert result[1] == ['2025-10-21']
|
|
157
|
+
assert isinstance(result[1][0], str)
|
|
114
158
|
|
|
115
159
|
|
|
116
160
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import string
|
|
3
|
+
import datetime
|
|
3
4
|
from typing import NamedTuple, Tuple, Any, List, Dict, Iterable, Union, TYPE_CHECKING
|
|
4
5
|
from string.templatelib import Template, Interpolation
|
|
5
6
|
|
|
@@ -127,6 +128,8 @@ class TSQL:
|
|
|
127
128
|
return [Parameter(val.expression, value)]
|
|
128
129
|
case _, int():
|
|
129
130
|
return [Parameter(val.expression, val.value)]
|
|
131
|
+
case '', datetime.datetime() | datetime.date() | datetime.time() | datetime.timedelta():
|
|
132
|
+
return [Parameter(val.expression, value)]
|
|
130
133
|
case _, _:
|
|
131
134
|
return [Parameter(val.expression, formatter.format_field(value, val.format_spec))]
|
|
132
135
|
|
|
@@ -179,7 +182,20 @@ def as_values(value_dict: dict[str, Any]):
|
|
|
179
182
|
for i, value in enumerate(values):
|
|
180
183
|
if i > 0:
|
|
181
184
|
value_parts.append(', ')
|
|
182
|
-
|
|
185
|
+
|
|
186
|
+
# Handle special types that should be inlined as SQL
|
|
187
|
+
if isinstance(value, Template):
|
|
188
|
+
# Inline the Template by processing it through _sqlize
|
|
189
|
+
value_parts.extend(TSQL._sqlize(value))
|
|
190
|
+
elif isinstance(value, TSQL):
|
|
191
|
+
# Inline the TSQL object's parts directly
|
|
192
|
+
value_parts.extend(value._sql_parts)
|
|
193
|
+
elif hasattr(value, 'to_tsql'):
|
|
194
|
+
# Handle QueryBuilder objects
|
|
195
|
+
value_parts.extend(value.to_tsql()._sql_parts)
|
|
196
|
+
else:
|
|
197
|
+
# Normal value - create Parameter
|
|
198
|
+
value_parts.append(Parameter(f'value_{i}', value))
|
|
183
199
|
value_parts.append(')')
|
|
184
200
|
|
|
185
201
|
# Create TSQL object manually
|
|
@@ -208,7 +224,20 @@ def as_set(value_dict: dict[str, Any]):
|
|
|
208
224
|
set_parts.append(', ')
|
|
209
225
|
set_parts.append(key)
|
|
210
226
|
set_parts.append(' = ')
|
|
211
|
-
|
|
227
|
+
|
|
228
|
+
# Handle special types that should be inlined as SQL
|
|
229
|
+
if isinstance(value, Template):
|
|
230
|
+
# Inline the Template by processing it through _sqlize
|
|
231
|
+
set_parts.extend(TSQL._sqlize(value))
|
|
232
|
+
elif isinstance(value, TSQL):
|
|
233
|
+
# Inline the TSQL object's parts directly
|
|
234
|
+
set_parts.extend(value._sql_parts)
|
|
235
|
+
elif hasattr(value, 'to_tsql'):
|
|
236
|
+
# Handle QueryBuilder objects
|
|
237
|
+
set_parts.extend(value.to_tsql()._sql_parts)
|
|
238
|
+
else:
|
|
239
|
+
# Normal value - create Parameter
|
|
240
|
+
set_parts.append(Parameter(f'value_{i}', value))
|
|
212
241
|
|
|
213
242
|
# Create TSQL object manually
|
|
214
243
|
tsql_obj = TSQL.__new__(TSQL)
|
|
@@ -719,11 +719,8 @@ class InsertBuilder(QueryBuilder):
|
|
|
719
719
|
# Apply type processors to values
|
|
720
720
|
values_dict = {}
|
|
721
721
|
for col_name, value in self.values.items():
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
values_dict[col_name] = processor.process_bind_param(value)
|
|
725
|
-
else:
|
|
726
|
-
values_dict[col_name] = value
|
|
722
|
+
processor = self.base_table._type_processors.get(col_name)
|
|
723
|
+
values_dict[col_name] = _process_value_for_builder(value, processor)
|
|
727
724
|
|
|
728
725
|
# MySQL INSERT IGNORE
|
|
729
726
|
if self._ignore:
|
|
@@ -758,11 +755,8 @@ class InsertBuilder(QueryBuilder):
|
|
|
758
755
|
# User specified which columns to update - apply type processors
|
|
759
756
|
update_dict = {}
|
|
760
757
|
for col_name, value in self._update_cols.items():
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
update_dict[col_name] = processor.process_bind_param(value)
|
|
764
|
-
else:
|
|
765
|
-
update_dict[col_name] = value
|
|
758
|
+
processor = self.base_table._type_processors.get(col_name)
|
|
759
|
+
update_dict[col_name] = _process_value_for_builder(value, processor)
|
|
766
760
|
parts.append(t'ON CONFLICT ({conflict_cols_str:unsafe}) DO UPDATE SET {update_dict:as_set}')
|
|
767
761
|
else:
|
|
768
762
|
# Default: update all non-conflict columns with EXCLUDED.*
|
|
@@ -786,11 +780,8 @@ class InsertBuilder(QueryBuilder):
|
|
|
786
780
|
# Apply type processors
|
|
787
781
|
update_dict = {}
|
|
788
782
|
for col_name, value in self._update_cols.items():
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
update_dict[col_name] = processor.process_bind_param(value)
|
|
792
|
-
else:
|
|
793
|
-
update_dict[col_name] = value
|
|
783
|
+
processor = self.base_table._type_processors.get(col_name)
|
|
784
|
+
update_dict[col_name] = _process_value_for_builder(value, processor)
|
|
794
785
|
parts.append(t'ON DUPLICATE KEY UPDATE {update_dict:as_set}')
|
|
795
786
|
else:
|
|
796
787
|
# Default: update all columns with alias.column (new MySQL syntax)
|
|
@@ -830,6 +821,30 @@ class InsertBuilder(QueryBuilder):
|
|
|
830
821
|
return f"InsertBuilder(<error rendering: {e}>)"
|
|
831
822
|
|
|
832
823
|
|
|
824
|
+
def _process_value_for_builder(value: Any, type_processor: Any = None) -> Any:
|
|
825
|
+
"""Apply type processor to a value if appropriate.
|
|
826
|
+
|
|
827
|
+
This helper ensures Template, TSQL, and QueryBuilder objects are not processed
|
|
828
|
+
by TypeProcessors, allowing them to be inlined as SQL instead of parameterized.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
value: The value to potentially process
|
|
832
|
+
type_processor: Optional TypeProcessor instance
|
|
833
|
+
|
|
834
|
+
Returns:
|
|
835
|
+
Processed value (or unchanged if special type or no processor)
|
|
836
|
+
"""
|
|
837
|
+
# Don't process special types that should be inlined as SQL
|
|
838
|
+
if isinstance(value, (Column, Template)) or hasattr(value, 'to_tsql'):
|
|
839
|
+
return value
|
|
840
|
+
|
|
841
|
+
# Apply processor if present (this handles None correctly - processors can transform it)
|
|
842
|
+
if type_processor is not None:
|
|
843
|
+
return type_processor.process_bind_param(value)
|
|
844
|
+
|
|
845
|
+
return value
|
|
846
|
+
|
|
847
|
+
|
|
833
848
|
class UpdateBuilder(QueryBuilder):
|
|
834
849
|
"""Fluent interface for building UPDATE queries"""
|
|
835
850
|
|
|
@@ -913,11 +928,8 @@ class UpdateBuilder(QueryBuilder):
|
|
|
913
928
|
# Apply type processors to values
|
|
914
929
|
values_dict = {}
|
|
915
930
|
for col_name, value in self.values.items():
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
values_dict[col_name] = processor.process_bind_param(value)
|
|
919
|
-
else:
|
|
920
|
-
values_dict[col_name] = value
|
|
931
|
+
processor = self.base_table._type_processors.get(col_name)
|
|
932
|
+
values_dict[col_name] = _process_value_for_builder(value, processor)
|
|
921
933
|
|
|
922
934
|
parts.append(t'UPDATE {table_name:literal} SET {values_dict:as_set}')
|
|
923
935
|
|
|
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
|