velocity-python 0.0.132__py3-none-any.whl → 0.0.134__py3-none-any.whl
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.
Potentially problematic release.
This version of velocity-python might be problematic. Click here for more details.
- velocity/__init__.py +1 -1
- velocity/app/tests/__init__.py +1 -0
- velocity/app/tests/test_email_processing.py +112 -0
- velocity/app/tests/test_payment_profile_sorting.py +191 -0
- velocity/app/tests/test_spreadsheet_functions.py +124 -0
- velocity/aws/tests/__init__.py +1 -0
- velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
- velocity/aws/tests/test_response.py +163 -0
- velocity/db/core/decorators.py +20 -3
- velocity/db/core/engine.py +33 -7
- velocity/db/exceptions.py +7 -0
- velocity/db/servers/base/initializer.py +2 -1
- velocity/db/servers/mysql/__init__.py +13 -4
- velocity/db/servers/postgres/__init__.py +14 -4
- velocity/db/servers/sqlite/__init__.py +13 -4
- velocity/db/servers/sqlserver/__init__.py +13 -4
- velocity/db/tests/__init__.py +1 -0
- velocity/db/tests/common_db_test.py +0 -0
- velocity/db/tests/postgres/__init__.py +1 -0
- velocity/db/tests/postgres/common.py +49 -0
- velocity/db/tests/postgres/test_column.py +29 -0
- velocity/db/tests/postgres/test_connections.py +25 -0
- velocity/db/tests/postgres/test_database.py +21 -0
- velocity/db/tests/postgres/test_engine.py +205 -0
- velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity/db/tests/postgres/test_imports.py +8 -0
- velocity/db/tests/postgres/test_result.py +19 -0
- velocity/db/tests/postgres/test_row.py +137 -0
- velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity/db/tests/sql/__init__.py +1 -0
- velocity/db/tests/sql/common.py +177 -0
- velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
- velocity/db/tests/test_db_utils.py +221 -0
- velocity/db/tests/test_postgres.py +212 -0
- velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity/db/tests/test_process_error_robustness.py +292 -0
- velocity/db/tests/test_result_caching.py +279 -0
- velocity/db/tests/test_result_sql_aware.py +117 -0
- velocity/db/tests/test_row_get_missing_column.py +72 -0
- velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity/db/tests/test_sql_builder.py +165 -0
- velocity/db/tests/test_tablehelper.py +486 -0
- velocity/misc/tests/__init__.py +1 -0
- velocity/misc/tests/test_db.py +90 -0
- velocity/misc/tests/test_fix.py +78 -0
- velocity/misc/tests/test_format.py +64 -0
- velocity/misc/tests/test_iconv.py +203 -0
- velocity/misc/tests/test_merge.py +82 -0
- velocity/misc/tests/test_oconv.py +144 -0
- velocity/misc/tests/test_original_error.py +52 -0
- velocity/misc/tests/test_timer.py +74 -0
- {velocity_python-0.0.132.dist-info → velocity_python-0.0.134.dist-info}/METADATA +1 -1
- velocity_python-0.0.134.dist-info/RECORD +125 -0
- velocity_python-0.0.132.dist-info/RECORD +0 -76
- {velocity_python-0.0.132.dist-info → velocity_python-0.0.134.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.132.dist-info → velocity_python-0.0.134.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.132.dist-info → velocity_python-0.0.134.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Comprehensive test suite for TableHelper class in velocity-python
|
|
4
|
+
|
|
5
|
+
Tests the core functionality including:
|
|
6
|
+
- Column name extraction from SQL expressions
|
|
7
|
+
- Reference resolution with pointer syntax
|
|
8
|
+
- Operator handling
|
|
9
|
+
- Aggregate function support
|
|
10
|
+
- Edge cases and error conditions
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import unittest
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
# Add the src directory to Python path for imports
|
|
18
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
19
|
+
|
|
20
|
+
from velocity.db.servers.tablehelper import TableHelper
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MockTransaction:
|
|
24
|
+
"""Mock transaction object for testing TableHelper"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestTableHelper(unittest.TestCase):
|
|
31
|
+
"""Test suite for TableHelper class"""
|
|
32
|
+
|
|
33
|
+
def setUp(self):
|
|
34
|
+
"""Set up test fixtures"""
|
|
35
|
+
self.tx = MockTransaction()
|
|
36
|
+
self.helper = TableHelper(self.tx, "test_table")
|
|
37
|
+
|
|
38
|
+
# Set up some mock operators for testing (based on postgres operators.py)
|
|
39
|
+
# Note: Order matters - longer operators should be checked first
|
|
40
|
+
self.helper.operators = {
|
|
41
|
+
"<>": "<>",
|
|
42
|
+
"!=": "<>",
|
|
43
|
+
"!><": "NOT BETWEEN",
|
|
44
|
+
">!<": "NOT BETWEEN",
|
|
45
|
+
"><": "BETWEEN",
|
|
46
|
+
"%%": "ILIKE",
|
|
47
|
+
"!%%": "NOT ILIKE",
|
|
48
|
+
"==": "=",
|
|
49
|
+
"<=": "<=",
|
|
50
|
+
">=": ">=",
|
|
51
|
+
"<": "<",
|
|
52
|
+
">": ">",
|
|
53
|
+
"!~*": "!~*",
|
|
54
|
+
"~*": "~*",
|
|
55
|
+
"!~": "!~",
|
|
56
|
+
"%": "LIKE",
|
|
57
|
+
"!%": "NOT LIKE",
|
|
58
|
+
"~": "~",
|
|
59
|
+
"=": "=",
|
|
60
|
+
"!": "<>",
|
|
61
|
+
"#": "ILIKE",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def test_extract_column_name_simple_columns(self):
|
|
65
|
+
"""Test extracting column names from simple expressions"""
|
|
66
|
+
test_cases = [
|
|
67
|
+
("column_name", "column_name"),
|
|
68
|
+
("id", "id"),
|
|
69
|
+
("user_id", "user_id"),
|
|
70
|
+
("created_at", "created_at"),
|
|
71
|
+
("table.column", "table.column"),
|
|
72
|
+
# Note: schema.table.column extracts 'schema.table' due to regex limitations
|
|
73
|
+
("schema.table.column", "schema.table"),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
for input_expr, expected in test_cases:
|
|
77
|
+
with self.subTest(expr=input_expr):
|
|
78
|
+
result = self.helper.extract_column_name(input_expr)
|
|
79
|
+
self.assertEqual(
|
|
80
|
+
result,
|
|
81
|
+
expected,
|
|
82
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def test_extract_column_name_asterisk(self):
|
|
86
|
+
"""Test extracting column names from asterisk expressions"""
|
|
87
|
+
test_cases = [
|
|
88
|
+
("*", "*"),
|
|
89
|
+
# Note: table.* extracts 'table' due to regex behavior
|
|
90
|
+
("table.*", "table"),
|
|
91
|
+
("schema.table.*", "schema.table"),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for input_expr, expected in test_cases:
|
|
95
|
+
with self.subTest(expr=input_expr):
|
|
96
|
+
result = self.helper.extract_column_name(input_expr)
|
|
97
|
+
self.assertEqual(
|
|
98
|
+
result,
|
|
99
|
+
expected,
|
|
100
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def test_extract_column_name_aggregate_functions(self):
|
|
104
|
+
"""Test extracting column names from aggregate function expressions"""
|
|
105
|
+
test_cases = [
|
|
106
|
+
("count(*)", "*"),
|
|
107
|
+
("count(id)", "id"),
|
|
108
|
+
("sum(amount)", "amount"),
|
|
109
|
+
("max(created_date)", "created_date"),
|
|
110
|
+
("min(user_id)", "user_id"),
|
|
111
|
+
("avg(score)", "score"),
|
|
112
|
+
("count(table.column)", "table.column"),
|
|
113
|
+
# Note: schema.table.amount extracts 'schema.table' due to regex behavior
|
|
114
|
+
("sum(schema.table.amount)", "schema.table"),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
for input_expr, expected in test_cases:
|
|
118
|
+
with self.subTest(expr=input_expr):
|
|
119
|
+
result = self.helper.extract_column_name(input_expr)
|
|
120
|
+
self.assertEqual(
|
|
121
|
+
result,
|
|
122
|
+
expected,
|
|
123
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def test_extract_column_name_nested_functions(self):
|
|
127
|
+
"""Test extracting column names from nested function expressions"""
|
|
128
|
+
test_cases = [
|
|
129
|
+
("sum(count(id))", "id"),
|
|
130
|
+
("max(sum(amount))", "amount"),
|
|
131
|
+
("coalesce(column_name, 0)", "column_name"),
|
|
132
|
+
("coalesce(sum(amount), 0)", "amount"),
|
|
133
|
+
("nvl(max(score), -1)", "score"),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
for input_expr, expected in test_cases:
|
|
137
|
+
with self.subTest(expr=input_expr):
|
|
138
|
+
result = self.helper.extract_column_name(input_expr)
|
|
139
|
+
self.assertEqual(
|
|
140
|
+
result,
|
|
141
|
+
expected,
|
|
142
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def test_extract_column_name_pointer_syntax(self):
|
|
146
|
+
"""Test extracting column names with pointer syntax (foreign key references)"""
|
|
147
|
+
test_cases = [
|
|
148
|
+
("parent_id>parent_name", "parent_id>parent_name"),
|
|
149
|
+
("user_id>username", "user_id>username"),
|
|
150
|
+
("category_id>category_name", "category_id>category_name"),
|
|
151
|
+
("count(parent_id>parent_name)", "parent_id>parent_name"),
|
|
152
|
+
("sum(user_id>score)", "user_id>score"),
|
|
153
|
+
("max(category_id>sort_order)", "category_id>sort_order"),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
for input_expr, expected in test_cases:
|
|
157
|
+
with self.subTest(expr=input_expr):
|
|
158
|
+
result = self.helper.extract_column_name(input_expr)
|
|
159
|
+
self.assertEqual(
|
|
160
|
+
result,
|
|
161
|
+
expected,
|
|
162
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def test_extract_column_name_with_aliases(self):
|
|
166
|
+
"""Test extracting column names from expressions with aliases"""
|
|
167
|
+
test_cases = [
|
|
168
|
+
("count(*) as total_count", "*"),
|
|
169
|
+
("sum(amount) as total_amount", "amount"),
|
|
170
|
+
("user_id as id", "user_id"),
|
|
171
|
+
("table.column as col", "table.column"),
|
|
172
|
+
("count(parent_id>parent_name) as parent_count", "parent_id>parent_name"),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
for input_expr, expected in test_cases:
|
|
176
|
+
with self.subTest(expr=input_expr):
|
|
177
|
+
result = self.helper.extract_column_name(input_expr)
|
|
178
|
+
self.assertEqual(
|
|
179
|
+
result,
|
|
180
|
+
expected,
|
|
181
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def test_extract_column_name_case_expressions(self):
|
|
185
|
+
"""Test extracting column names from CASE expressions"""
|
|
186
|
+
test_cases = [
|
|
187
|
+
('CASE WHEN status = "active" THEN 1 ELSE 0 END', "status"),
|
|
188
|
+
('sum(CASE WHEN status = "active" THEN amount ELSE 0 END)', "status"),
|
|
189
|
+
('CASE WHEN user_id>role = "admin" THEN 1 ELSE 0 END', "user_id>role"),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
for input_expr, expected in test_cases:
|
|
193
|
+
with self.subTest(expr=input_expr):
|
|
194
|
+
result = self.helper.extract_column_name(input_expr)
|
|
195
|
+
self.assertEqual(
|
|
196
|
+
result,
|
|
197
|
+
expected,
|
|
198
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def test_extract_column_name_cast_expressions(self):
|
|
202
|
+
"""Test extracting column names from CAST expressions"""
|
|
203
|
+
test_cases = [
|
|
204
|
+
("CAST(amount AS DECIMAL)", "amount"),
|
|
205
|
+
("CAST(created_date AS VARCHAR)", "created_date"),
|
|
206
|
+
("sum(CAST(amount AS DECIMAL))", "amount"),
|
|
207
|
+
("CAST(user_id>score AS INTEGER)", "user_id>score"),
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
for input_expr, expected in test_cases:
|
|
211
|
+
with self.subTest(expr=input_expr):
|
|
212
|
+
result = self.helper.extract_column_name(input_expr)
|
|
213
|
+
self.assertEqual(
|
|
214
|
+
result,
|
|
215
|
+
expected,
|
|
216
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def test_extract_column_name_edge_cases(self):
|
|
220
|
+
"""Test edge cases for column name extraction"""
|
|
221
|
+
test_cases = [
|
|
222
|
+
("", None), # Empty string
|
|
223
|
+
(" ", None), # Whitespace only
|
|
224
|
+
("123invalid", None), # Invalid identifier
|
|
225
|
+
# Note: count() actually extracts 'count' as function name
|
|
226
|
+
("count()", "count"),
|
|
227
|
+
# Note: malformed function call extracts function name
|
|
228
|
+
("invalid_function_call(", "invalid_function_call"),
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
for input_expr, expected in test_cases:
|
|
232
|
+
with self.subTest(expr=input_expr):
|
|
233
|
+
result = self.helper.extract_column_name(input_expr)
|
|
234
|
+
self.assertEqual(
|
|
235
|
+
result,
|
|
236
|
+
expected,
|
|
237
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def test_remove_operator(self):
|
|
241
|
+
"""Test removing operator prefixes from expressions"""
|
|
242
|
+
test_cases = [
|
|
243
|
+
(">count(*)", "count(*)"),
|
|
244
|
+
("!status", "status"),
|
|
245
|
+
# remove_operator removes the entire operator prefix
|
|
246
|
+
("!=amount", "amount"), # != is completely removed
|
|
247
|
+
(">=created_date", "created_date"), # >= is completely removed
|
|
248
|
+
("<=score", "score"), # <= is completely removed
|
|
249
|
+
("<user_id", "user_id"),
|
|
250
|
+
("normal_column", "normal_column"), # No operator
|
|
251
|
+
("", ""), # Empty string
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
for input_expr, expected in test_cases:
|
|
255
|
+
with self.subTest(expr=input_expr):
|
|
256
|
+
result = self.helper.remove_operator(input_expr)
|
|
257
|
+
self.assertEqual(
|
|
258
|
+
result,
|
|
259
|
+
expected,
|
|
260
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def test_has_pointer(self):
|
|
264
|
+
"""Test detection of pointer syntax in expressions"""
|
|
265
|
+
test_cases = [
|
|
266
|
+
("parent_id>parent_name", True),
|
|
267
|
+
("user_id>username", True),
|
|
268
|
+
("category_id>name", True),
|
|
269
|
+
("normal_column", False),
|
|
270
|
+
("table.column", False),
|
|
271
|
+
("count(*)", False),
|
|
272
|
+
("sum(amount)", False),
|
|
273
|
+
(">", False), # Just operator
|
|
274
|
+
("column>", False), # Incomplete pointer
|
|
275
|
+
(">column", False), # Invalid pointer
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
for input_expr, expected in test_cases:
|
|
279
|
+
with self.subTest(expr=input_expr):
|
|
280
|
+
result = self.helper.has_pointer(input_expr)
|
|
281
|
+
self.assertEqual(
|
|
282
|
+
result,
|
|
283
|
+
expected,
|
|
284
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def test_resolve_references_simple(self):
|
|
288
|
+
"""Test basic reference resolution without foreign keys"""
|
|
289
|
+
test_cases = [
|
|
290
|
+
("column_name", "column_name"),
|
|
291
|
+
("count(*)", "count(*)"),
|
|
292
|
+
("sum(amount)", "sum(amount)"),
|
|
293
|
+
("table.column", "table.column"),
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
for input_expr, expected in test_cases:
|
|
297
|
+
with self.subTest(expr=input_expr):
|
|
298
|
+
# Use bypass_on_error to avoid foreign key lookup failures in tests
|
|
299
|
+
result = self.helper.resolve_references(
|
|
300
|
+
input_expr, options={"bypass_on_error": True}
|
|
301
|
+
)
|
|
302
|
+
# For simple tests, we expect the expression to be preserved
|
|
303
|
+
self.assertIsNotNone(result, f"Failed for '{input_expr}': got None")
|
|
304
|
+
|
|
305
|
+
def test_resolve_references_with_operators(self):
|
|
306
|
+
"""Test reference resolution with operator prefixes"""
|
|
307
|
+
test_cases = [
|
|
308
|
+
(">count(*)", "count(*)"),
|
|
309
|
+
("!status", "status"),
|
|
310
|
+
(">=amount", "amount"),
|
|
311
|
+
("!=user_id", "user_id"),
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
for input_expr, expected_contains in test_cases:
|
|
315
|
+
with self.subTest(expr=input_expr):
|
|
316
|
+
# Use bypass_on_error to avoid foreign key lookup failures
|
|
317
|
+
result = self.helper.resolve_references(
|
|
318
|
+
input_expr, options={"bypass_on_error": True}
|
|
319
|
+
)
|
|
320
|
+
# The result should contain the column part without the operator
|
|
321
|
+
self.assertIsNotNone(result, f"Failed for '{input_expr}': got None")
|
|
322
|
+
# We can't predict exact output due to quoting, but it should not error
|
|
323
|
+
|
|
324
|
+
def test_get_operator(self):
|
|
325
|
+
"""Test operator detection from expressions"""
|
|
326
|
+
test_cases = [
|
|
327
|
+
(">value", "any_val", ">"),
|
|
328
|
+
("<value", "any_val", "<"),
|
|
329
|
+
("!value", "any_val", "<>"),
|
|
330
|
+
("!=value", "any_val", "<>"),
|
|
331
|
+
(">=value", "any_val", ">="),
|
|
332
|
+
("<=value", "any_val", "<="),
|
|
333
|
+
("normal_value", "any_val", "="), # Default operator
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
for input_expr, test_val, expected in test_cases:
|
|
337
|
+
with self.subTest(expr=input_expr):
|
|
338
|
+
result = self.helper.get_operator(input_expr, test_val)
|
|
339
|
+
self.assertEqual(
|
|
340
|
+
result,
|
|
341
|
+
expected,
|
|
342
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def test_are_parentheses_balanced(self):
|
|
346
|
+
"""Test parentheses balance checking"""
|
|
347
|
+
test_cases = [
|
|
348
|
+
("count(*)", True),
|
|
349
|
+
("sum(amount)", True),
|
|
350
|
+
("func(a, func2(b, c))", True),
|
|
351
|
+
("(a + b) * (c + d)", True),
|
|
352
|
+
("count(", False),
|
|
353
|
+
("sum(amount))", False),
|
|
354
|
+
("func(a, func2(b, c)", False),
|
|
355
|
+
("((unbalanced)", False),
|
|
356
|
+
("", True), # Empty string is balanced
|
|
357
|
+
("no_parens", True), # No parentheses is balanced
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
for input_expr, expected in test_cases:
|
|
361
|
+
with self.subTest(expr=input_expr):
|
|
362
|
+
result = self.helper.are_parentheses_balanced(input_expr)
|
|
363
|
+
self.assertEqual(
|
|
364
|
+
result,
|
|
365
|
+
expected,
|
|
366
|
+
f"Failed for '{input_expr}': expected '{expected}', got '{result}'",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class TestTableHelperIntegration(unittest.TestCase):
|
|
371
|
+
"""Integration tests for TableHelper with realistic scenarios"""
|
|
372
|
+
|
|
373
|
+
def setUp(self):
|
|
374
|
+
"""Set up test fixtures"""
|
|
375
|
+
self.tx = MockTransaction()
|
|
376
|
+
self.helper = TableHelper(self.tx, "orders")
|
|
377
|
+
|
|
378
|
+
# Set up operators (based on postgres operators.py)
|
|
379
|
+
# Note: Order matters - longer operators should be checked first
|
|
380
|
+
self.helper.operators = {
|
|
381
|
+
"<>": "<>",
|
|
382
|
+
"!=": "<>",
|
|
383
|
+
"!><": "NOT BETWEEN",
|
|
384
|
+
">!<": "NOT BETWEEN",
|
|
385
|
+
"><": "BETWEEN",
|
|
386
|
+
"%%": "ILIKE",
|
|
387
|
+
"!%%": "NOT ILIKE",
|
|
388
|
+
"==": "=",
|
|
389
|
+
"<=": "<=",
|
|
390
|
+
">=": ">=",
|
|
391
|
+
"<": "<",
|
|
392
|
+
">": ">",
|
|
393
|
+
"!~*": "!~*",
|
|
394
|
+
"~*": "~*",
|
|
395
|
+
"!~": "!~",
|
|
396
|
+
"%": "LIKE",
|
|
397
|
+
"!%": "NOT LIKE",
|
|
398
|
+
"~": "~",
|
|
399
|
+
"=": "=",
|
|
400
|
+
"!": "<>",
|
|
401
|
+
"#": "ILIKE",
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
def test_real_world_expressions(self):
|
|
405
|
+
"""Test with real-world SQL expressions that might be encountered"""
|
|
406
|
+
# These are expressions that should work without errors
|
|
407
|
+
expressions = [
|
|
408
|
+
"count(*)",
|
|
409
|
+
"sum(order_amount)",
|
|
410
|
+
"max(created_date)",
|
|
411
|
+
"count(customer_id>customer_name)",
|
|
412
|
+
"sum(line_items.quantity * line_items.price)",
|
|
413
|
+
'avg(CASE WHEN status = "completed" THEN order_amount ELSE 0 END)',
|
|
414
|
+
"coalesce(discount_amount, 0)",
|
|
415
|
+
">count(*)", # This was the original failing case
|
|
416
|
+
"!status",
|
|
417
|
+
">=order_date",
|
|
418
|
+
"!=customer_id",
|
|
419
|
+
]
|
|
420
|
+
|
|
421
|
+
for expr in expressions:
|
|
422
|
+
with self.subTest(expr=expr):
|
|
423
|
+
try:
|
|
424
|
+
# Test that extract_column_name doesn't crash
|
|
425
|
+
column = self.helper.extract_column_name(
|
|
426
|
+
self.helper.remove_operator(expr)
|
|
427
|
+
)
|
|
428
|
+
self.assertIsNotNone(
|
|
429
|
+
column, f"extract_column_name returned None for '{expr}'"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Test that resolve_references doesn't crash with bypass_on_error
|
|
433
|
+
result = self.helper.resolve_references(
|
|
434
|
+
expr, options={"bypass_on_error": True}
|
|
435
|
+
)
|
|
436
|
+
self.assertIsNotNone(
|
|
437
|
+
result, f"resolve_references returned None for '{expr}'"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
self.fail(f"Expression '{expr}' raised exception: {e}")
|
|
442
|
+
|
|
443
|
+
def test_count_star_specific_issue(self):
|
|
444
|
+
"""Test the specific issue that was reported: count(*) with operators"""
|
|
445
|
+
# This was the exact error: "Could not extract column name from: >count(*)"
|
|
446
|
+
problematic_expressions = [
|
|
447
|
+
">count(*)",
|
|
448
|
+
"!count(*)",
|
|
449
|
+
">=count(*)",
|
|
450
|
+
"!=count(*)",
|
|
451
|
+
"<count(*)",
|
|
452
|
+
"<=count(*)",
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
for expr in problematic_expressions:
|
|
456
|
+
with self.subTest(expr=expr):
|
|
457
|
+
try:
|
|
458
|
+
# This should not raise "Could not extract column name from..." error
|
|
459
|
+
result = self.helper.resolve_references(
|
|
460
|
+
expr, options={"bypass_on_error": True}
|
|
461
|
+
)
|
|
462
|
+
self.assertIsNotNone(
|
|
463
|
+
result, f"resolve_references failed for '{expr}'"
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# The result should contain 'count(*)'
|
|
467
|
+
self.assertIn(
|
|
468
|
+
"count(*)",
|
|
469
|
+
result,
|
|
470
|
+
f"Result '{result}' doesn't contain 'count(*)' for expr '{expr}'",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
except ValueError as e:
|
|
474
|
+
if "Could not extract column name from:" in str(e):
|
|
475
|
+
self.fail(f"The original error still occurs for '{expr}': {e}")
|
|
476
|
+
else:
|
|
477
|
+
# Other ValueError types are acceptable (e.g., foreign key issues)
|
|
478
|
+
pass
|
|
479
|
+
except Exception as e:
|
|
480
|
+
# Other exceptions are also acceptable in test environment
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
if __name__ == "__main__":
|
|
485
|
+
# Set up test discovery and run
|
|
486
|
+
unittest.main(verbosity=2, buffer=True)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Misc module tests
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
import numbers
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from velocity.db import exceptions
|
|
6
|
+
from ..db import NOTNULL, join, randomword, return_default, NotSupported
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestNotSupported(unittest.TestCase):
|
|
10
|
+
def test_not_supported(self):
|
|
11
|
+
with self.assertRaises(Exception) as context:
|
|
12
|
+
NotSupported()
|
|
13
|
+
self.assertEqual(
|
|
14
|
+
str(context.exception),
|
|
15
|
+
"Sorry, the driver for this database is not installed",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestNOTNULL(unittest.TestCase):
|
|
20
|
+
def test_notnull(self):
|
|
21
|
+
self.assertTrue(NOTNULL(("key", "value")))
|
|
22
|
+
self.assertFalse(NOTNULL(("key", None)))
|
|
23
|
+
self.assertFalse(NOTNULL(("key",)))
|
|
24
|
+
self.assertFalse(NOTNULL(()))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestJoin(unittest.TestCase):
|
|
28
|
+
def test_or(self):
|
|
29
|
+
self.assertEqual(join._or("a=1", "b=2"), "(a=1 or b=2)")
|
|
30
|
+
|
|
31
|
+
def test_and(self):
|
|
32
|
+
self.assertEqual(join._and("a=1", "b=2"), "(a=1 and b=2)")
|
|
33
|
+
|
|
34
|
+
def test_list(self):
|
|
35
|
+
result = join._list("a=1", key1=123, key2="value")
|
|
36
|
+
self.assertIn("a=1", result)
|
|
37
|
+
self.assertIn("key1=123", result)
|
|
38
|
+
self.assertIn("key2='value'", result)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestRandomWord(unittest.TestCase):
|
|
42
|
+
def test_randomword_length_specified(self):
|
|
43
|
+
word = randomword(10)
|
|
44
|
+
self.assertEqual(len(word), 10)
|
|
45
|
+
self.assertTrue(word.islower())
|
|
46
|
+
|
|
47
|
+
def test_randomword_length_random(self):
|
|
48
|
+
word = randomword()
|
|
49
|
+
self.assertTrue(5 <= len(word) <= 15)
|
|
50
|
+
self.assertTrue(word.islower())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestReturnDefault(unittest.TestCase):
|
|
54
|
+
def setUp(self):
|
|
55
|
+
class MockTransaction:
|
|
56
|
+
def create_savepoint(self, cursor):
|
|
57
|
+
return "savepoint"
|
|
58
|
+
|
|
59
|
+
def rollback_savepoint(self, sp, cursor):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def release_savepoint(self, sp, cursor):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
class MockTable:
|
|
66
|
+
cursor = MagicMock()
|
|
67
|
+
|
|
68
|
+
class MockClass:
|
|
69
|
+
tx = MockTransaction()
|
|
70
|
+
table = MockTable()
|
|
71
|
+
|
|
72
|
+
@return_default(default="default_value")
|
|
73
|
+
def func(self, raise_exception=False):
|
|
74
|
+
if raise_exception:
|
|
75
|
+
raise exceptions.DbApplicationError("Test error")
|
|
76
|
+
return "result"
|
|
77
|
+
|
|
78
|
+
self.mock_obj = MockClass()
|
|
79
|
+
|
|
80
|
+
def test_return_default_no_exception(self):
|
|
81
|
+
result = self.mock_obj.func(raise_exception=False)
|
|
82
|
+
self.assertEqual(result, "result")
|
|
83
|
+
|
|
84
|
+
def test_return_default_with_exception(self):
|
|
85
|
+
result = self.mock_obj.func(raise_exception=True)
|
|
86
|
+
self.assertEqual(result, "default_value")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
unittest.main()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Test script to verify the duplicate_rows fix
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_grouping_fix():
|
|
7
|
+
"""Test the fixed grouping logic"""
|
|
8
|
+
|
|
9
|
+
# Simulate duplicate rows that would come from duplicate_rows()
|
|
10
|
+
duplicate_rows = [
|
|
11
|
+
{
|
|
12
|
+
"sys_id": 1,
|
|
13
|
+
"email_address": "test1@example.com",
|
|
14
|
+
"card_number": "1234",
|
|
15
|
+
"expiration_date": "2024-01",
|
|
16
|
+
"status": None,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"sys_id": 2,
|
|
20
|
+
"email_address": "test1@example.com",
|
|
21
|
+
"card_number": "1234",
|
|
22
|
+
"expiration_date": "2024-02",
|
|
23
|
+
"status": None,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"sys_id": 3,
|
|
27
|
+
"email_address": "test2@example.com",
|
|
28
|
+
"card_number": "5678",
|
|
29
|
+
"expiration_date": "2024-03",
|
|
30
|
+
"status": None,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"sys_id": 4,
|
|
34
|
+
"email_address": "test2@example.com",
|
|
35
|
+
"card_number": "5678",
|
|
36
|
+
"expiration_date": "2024-01",
|
|
37
|
+
"status": None,
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Group rows by email_address and card_number (the fixed logic)
|
|
42
|
+
groups = {}
|
|
43
|
+
for row in duplicate_rows:
|
|
44
|
+
key = (row["email_address"], row["card_number"])
|
|
45
|
+
if key not in groups:
|
|
46
|
+
groups[key] = []
|
|
47
|
+
groups[key].append(row)
|
|
48
|
+
|
|
49
|
+
print("Groups found:")
|
|
50
|
+
for key, group in groups.items():
|
|
51
|
+
print(f" Key: {key}, Group size: {len(group)}")
|
|
52
|
+
|
|
53
|
+
# Test the sorting that was causing the original error
|
|
54
|
+
try:
|
|
55
|
+
sorted_group = sorted(group, key=lambda x: x["expiration_date"])
|
|
56
|
+
print(
|
|
57
|
+
f" Sorted by expiration_date: {[row['expiration_date'] for row in sorted_group]}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Test the enumeration that happens in the original code
|
|
61
|
+
for idx, row in enumerate(sorted_group):
|
|
62
|
+
print(
|
|
63
|
+
f" {idx}: {row['sys_id']}, {row['email_address']}, {row['card_number']}, {row['expiration_date']}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
except TypeError as e:
|
|
67
|
+
print(f" ERROR: {e}")
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
success = test_grouping_fix()
|
|
75
|
+
if success:
|
|
76
|
+
print("\n✓ Fix appears to work correctly!")
|
|
77
|
+
else:
|
|
78
|
+
print("\n✗ Fix has issues")
|