velocity-python 0.0.109__py3-none-any.whl → 0.0.155__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.
- velocity/__init__.py +3 -1
- velocity/app/orders.py +3 -4
- 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/__init__.py +3 -0
- velocity/aws/amplify.py +10 -6
- velocity/aws/handlers/__init__.py +2 -0
- velocity/aws/handlers/base_handler.py +248 -0
- velocity/aws/handlers/context.py +167 -2
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +24 -85
- velocity/aws/handlers/mixins/__init__.py +16 -0
- velocity/aws/handlers/mixins/activity_tracker.py +181 -0
- velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity/aws/handlers/mixins/error_handler.py +192 -0
- velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
- velocity/aws/handlers/mixins/standard_mixin.py +73 -0
- velocity/aws/handlers/response.py +1 -1
- velocity/aws/handlers/sqs_handler.py +28 -143
- 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/__init__.py +16 -4
- velocity/db/core/decorators.py +20 -4
- velocity/db/core/engine.py +185 -839
- velocity/db/core/result.py +30 -24
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +279 -40
- velocity/db/core/transaction.py +19 -11
- velocity/db/exceptions.py +42 -18
- velocity/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +70 -0
- velocity/db/servers/base/operators.py +98 -0
- velocity/db/servers/base/sql.py +503 -0
- velocity/db/servers/base/types.py +135 -0
- velocity/db/servers/mysql/__init__.py +73 -0
- velocity/db/servers/mysql/operators.py +54 -0
- velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
- velocity/db/servers/mysql/sql.py +718 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +59 -11
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +474 -120
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +61 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +677 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +805 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- velocity/db/servers/tablehelper.py +117 -91
- 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_row_comprehensive.py +720 -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_sql_comprehensive.py +462 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_table_comprehensive.py +646 -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 +448 -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/db/utils.py +62 -47
- velocity/misc/conv/__init__.py +2 -0
- velocity/misc/conv/iconv.py +5 -4
- velocity/misc/export.py +1 -4
- velocity/misc/merge.py +1 -1
- 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/misc/tools.py +0 -1
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/METADATA +2 -2
- velocity_python-0.0.155.dist-info/RECORD +129 -0
- velocity/db/core/exceptions.py +0 -70
- velocity/db/servers/mysql.py +0 -641
- velocity/db/servers/sqlite.py +0 -968
- velocity/db/servers/sqlite_reserved.py +0 -208
- velocity/db/servers/sqlserver.py +0 -921
- velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.109.dist-info/RECORD +0 -56
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Quick test to verify PostgreSQL implementation is functionally unchanged.
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
sys.path.insert(0, '/home/ubuntu/tenspace/velocity-python/src')
|
|
8
|
+
|
|
9
|
+
def test_postgres_unchanged():
|
|
10
|
+
"""Test that PostgreSQL implementation is functionally unchanged."""
|
|
11
|
+
print("Testing PostgreSQL implementation...")
|
|
12
|
+
|
|
13
|
+
# Test imports
|
|
14
|
+
try:
|
|
15
|
+
from velocity.db.servers.postgres import initialize
|
|
16
|
+
from velocity.db.servers.postgres.sql import SQL
|
|
17
|
+
from velocity.db.servers.postgres.types import TYPES
|
|
18
|
+
from velocity.db.servers.postgres.operators import OPERATORS
|
|
19
|
+
print("✓ All imports successful")
|
|
20
|
+
except ImportError as e:
|
|
21
|
+
print(f"✗ Import failed: {e}")
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
# Test SQL class attributes are the same
|
|
25
|
+
expected_server = "PostGreSQL"
|
|
26
|
+
if SQL.server != expected_server:
|
|
27
|
+
print(f"✗ SQL.server changed: expected '{expected_server}', got '{SQL.server}'")
|
|
28
|
+
return False
|
|
29
|
+
print("✓ SQL.server unchanged")
|
|
30
|
+
|
|
31
|
+
# Test error codes are preserved
|
|
32
|
+
expected_duplicate_codes = ["23505"]
|
|
33
|
+
if SQL.DuplicateKeyErrorCodes != expected_duplicate_codes:
|
|
34
|
+
print(f"✗ DuplicateKeyErrorCodes changed: expected {expected_duplicate_codes}, got {SQL.DuplicateKeyErrorCodes}")
|
|
35
|
+
return False
|
|
36
|
+
print("✓ Error codes unchanged")
|
|
37
|
+
|
|
38
|
+
# Test TYPES class methods exist
|
|
39
|
+
if not hasattr(TYPES, 'get_type'):
|
|
40
|
+
print("✗ TYPES.get_type method missing")
|
|
41
|
+
return False
|
|
42
|
+
if not hasattr(TYPES, 'get_conv'):
|
|
43
|
+
print("✗ TYPES.get_conv method missing")
|
|
44
|
+
return False
|
|
45
|
+
if not hasattr(TYPES, 'py_type'):
|
|
46
|
+
print("✗ TYPES.py_type method missing")
|
|
47
|
+
return False
|
|
48
|
+
print("✓ TYPES methods present")
|
|
49
|
+
|
|
50
|
+
# Test type mappings are correct
|
|
51
|
+
if TYPES.get_type(str) != "TEXT":
|
|
52
|
+
print(f"✗ TYPES.get_type(str) changed: expected 'TEXT', got '{TYPES.get_type(str)}'")
|
|
53
|
+
return False
|
|
54
|
+
if TYPES.get_type(int) != "BIGINT":
|
|
55
|
+
print(f"✗ TYPES.get_type(int) changed: expected 'BIGINT', got '{TYPES.get_type(int)}'")
|
|
56
|
+
return False
|
|
57
|
+
print("✓ Type mappings unchanged")
|
|
58
|
+
|
|
59
|
+
# Test operators are preserved
|
|
60
|
+
if OPERATORS.get("<>") != "<>":
|
|
61
|
+
print(f"✗ Operator '<>' mapping changed")
|
|
62
|
+
return False
|
|
63
|
+
if OPERATORS.get("%%") != "ILIKE":
|
|
64
|
+
print(f"✗ Operator '%%' mapping changed")
|
|
65
|
+
return False
|
|
66
|
+
print("✓ Operators unchanged")
|
|
67
|
+
|
|
68
|
+
# Test SQL methods exist (just check key ones)
|
|
69
|
+
sql_methods = ['select', 'insert', 'update', 'delete', 'merge', 'version', 'databases']
|
|
70
|
+
for method in sql_methods:
|
|
71
|
+
if not hasattr(SQL, method):
|
|
72
|
+
print(f"✗ SQL.{method} method missing")
|
|
73
|
+
return False
|
|
74
|
+
print("✓ SQL methods present")
|
|
75
|
+
|
|
76
|
+
print("\n🎉 PostgreSQL implementation is functionally unchanged!")
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
success = test_postgres_unchanged()
|
|
81
|
+
sys.exit(0 if success else 1)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Test the robustness of the improved process_error method
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import Mock, patch
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
# Add the source directory to the path
|
|
13
|
+
sys.path.insert(0, "/home/ubuntu/tenspace/velocity-python/src")
|
|
14
|
+
|
|
15
|
+
from velocity.db.core.engine import Engine
|
|
16
|
+
from velocity.db import exceptions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MockException(Exception):
|
|
20
|
+
"""Mock exception for testing"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, message, pgcode=None, pgerror=None):
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.pgcode = pgcode
|
|
25
|
+
self.pgerror = pgerror
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MockSQL:
|
|
29
|
+
"""Mock SQL class for testing"""
|
|
30
|
+
|
|
31
|
+
server = "PostgreSQL"
|
|
32
|
+
|
|
33
|
+
ApplicationErrorCodes = ["22P02", "42883", "42501", "42601", "25P01", "25P02"]
|
|
34
|
+
DatabaseMissingErrorCodes = ["3D000"]
|
|
35
|
+
TableMissingErrorCodes = ["42P01"]
|
|
36
|
+
ColumnMissingErrorCodes = ["42703"]
|
|
37
|
+
ForeignKeyMissingErrorCodes = ["42704"]
|
|
38
|
+
ConnectionErrorCodes = [
|
|
39
|
+
"08001",
|
|
40
|
+
"08S01",
|
|
41
|
+
"57P03",
|
|
42
|
+
"08006",
|
|
43
|
+
"53300",
|
|
44
|
+
"08003",
|
|
45
|
+
"08004",
|
|
46
|
+
"08P01",
|
|
47
|
+
]
|
|
48
|
+
DuplicateKeyErrorCodes = ["23505"]
|
|
49
|
+
RetryTransactionCodes = ["40001", "40P01", "40002"]
|
|
50
|
+
TruncationErrorCodes = ["22001"]
|
|
51
|
+
LockTimeoutErrorCodes = ["55P03"]
|
|
52
|
+
DatabaseObjectExistsErrorCodes = ["42710", "42P07", "42P04"]
|
|
53
|
+
DataIntegrityErrorCodes = ["23503", "23502", "23514", "23P01", "22003"]
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_error(cls, e):
|
|
57
|
+
return getattr(e, "pgcode", None), getattr(e, "pgerror", None)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestProcessErrorRobustness(unittest.TestCase):
|
|
61
|
+
|
|
62
|
+
def setUp(self):
|
|
63
|
+
"""Set up test fixtures"""
|
|
64
|
+
self.engine = Engine(driver=Mock(), config={"test": "config"}, sql=MockSQL())
|
|
65
|
+
|
|
66
|
+
# Capture logs for testing
|
|
67
|
+
self.log_handler = logging.StreamHandler()
|
|
68
|
+
self.log_handler.setLevel(logging.DEBUG)
|
|
69
|
+
logger = logging.getLogger("velocity.db.engine")
|
|
70
|
+
logger.addHandler(self.log_handler)
|
|
71
|
+
logger.setLevel(logging.DEBUG)
|
|
72
|
+
|
|
73
|
+
def test_error_code_classification(self):
|
|
74
|
+
"""Test that error codes are properly classified"""
|
|
75
|
+
test_cases = [
|
|
76
|
+
# (pgcode, expected_exception_class, description)
|
|
77
|
+
("23505", exceptions.DbDuplicateKeyError, "unique violation"),
|
|
78
|
+
("40001", exceptions.DbRetryTransaction, "serialization failure"),
|
|
79
|
+
("40P01", exceptions.DbRetryTransaction, "deadlock detected"),
|
|
80
|
+
("42501", exceptions.DbApplicationError, "insufficient privilege"),
|
|
81
|
+
("42601", exceptions.DbApplicationError, "syntax error"),
|
|
82
|
+
("25P01", exceptions.DbApplicationError, "no active sql transaction"),
|
|
83
|
+
("3D000", exceptions.DbDatabaseMissingError, "invalid catalog name"),
|
|
84
|
+
("08003", exceptions.DbConnectionError, "connection does not exist"),
|
|
85
|
+
("23502", exceptions.DbDataIntegrityError, "not null violation"),
|
|
86
|
+
("42P01", exceptions.DbTableMissingError, "undefined table"),
|
|
87
|
+
("42703", exceptions.DbColumnMissingError, "undefined column"),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
for pgcode, expected_exception, description in test_cases:
|
|
91
|
+
with self.subTest(pgcode=pgcode, description=description):
|
|
92
|
+
mock_exc = MockException(f"Test error: {description}", pgcode=pgcode)
|
|
93
|
+
|
|
94
|
+
with patch(
|
|
95
|
+
"sys.exc_info", return_value=(type(mock_exc), mock_exc, None)
|
|
96
|
+
):
|
|
97
|
+
with self.assertRaises(expected_exception):
|
|
98
|
+
self.engine.process_error("test sql", {"param": "value"})
|
|
99
|
+
|
|
100
|
+
def test_regex_fallback_patterns(self):
|
|
101
|
+
"""Test regex pattern fallback when error codes aren't available"""
|
|
102
|
+
test_cases = [
|
|
103
|
+
# (message, expected_exception_class, description)
|
|
104
|
+
(
|
|
105
|
+
"key (sys_id)=(123) already exists.",
|
|
106
|
+
exceptions.DbDuplicateKeyError,
|
|
107
|
+
"sys_id duplicate",
|
|
108
|
+
),
|
|
109
|
+
(
|
|
110
|
+
"duplicate key value violates unique constraint",
|
|
111
|
+
exceptions.DbDuplicateKeyError,
|
|
112
|
+
"unique constraint",
|
|
113
|
+
),
|
|
114
|
+
(
|
|
115
|
+
"database 'testdb' does not exist",
|
|
116
|
+
exceptions.DbDatabaseMissingError,
|
|
117
|
+
"database missing",
|
|
118
|
+
),
|
|
119
|
+
(
|
|
120
|
+
"no such database: mydb",
|
|
121
|
+
exceptions.DbDatabaseMissingError,
|
|
122
|
+
"database not found",
|
|
123
|
+
),
|
|
124
|
+
(
|
|
125
|
+
"table 'users' already exists",
|
|
126
|
+
exceptions.DbObjectExistsError,
|
|
127
|
+
"object exists",
|
|
128
|
+
),
|
|
129
|
+
(
|
|
130
|
+
"server closed the connection unexpectedly",
|
|
131
|
+
exceptions.DbConnectionError,
|
|
132
|
+
"connection closed",
|
|
133
|
+
),
|
|
134
|
+
(
|
|
135
|
+
"connection timed out",
|
|
136
|
+
exceptions.DbConnectionError,
|
|
137
|
+
"connection timeout",
|
|
138
|
+
),
|
|
139
|
+
("no such table: users", exceptions.DbTableMissingError, "table missing"),
|
|
140
|
+
(
|
|
141
|
+
"permission denied for table users",
|
|
142
|
+
exceptions.DbApplicationError,
|
|
143
|
+
"permission denied",
|
|
144
|
+
),
|
|
145
|
+
(
|
|
146
|
+
"syntax error at or near 'SELCT'",
|
|
147
|
+
exceptions.DbApplicationError,
|
|
148
|
+
"syntax error",
|
|
149
|
+
),
|
|
150
|
+
("deadlock detected", exceptions.DbLockTimeoutError, "deadlock"),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for message, expected_exception, description in test_cases:
|
|
154
|
+
with self.subTest(message=message, description=description):
|
|
155
|
+
mock_exc = MockException(
|
|
156
|
+
message
|
|
157
|
+
) # No pgcode - will trigger regex fallback
|
|
158
|
+
|
|
159
|
+
with patch(
|
|
160
|
+
"sys.exc_info", return_value=(type(mock_exc), mock_exc, None)
|
|
161
|
+
):
|
|
162
|
+
with self.assertRaises(expected_exception):
|
|
163
|
+
self.engine.process_error("test sql", {"param": "value"})
|
|
164
|
+
|
|
165
|
+
def test_already_custom_exception(self):
|
|
166
|
+
"""Test that custom exceptions are re-raised as-is"""
|
|
167
|
+
custom_exc = exceptions.DbConnectionError("Already a custom exception")
|
|
168
|
+
|
|
169
|
+
with patch("sys.exc_info", return_value=(type(custom_exc), custom_exc, None)):
|
|
170
|
+
with self.assertRaises(exceptions.DbConnectionError):
|
|
171
|
+
self.engine.process_error()
|
|
172
|
+
|
|
173
|
+
def test_no_active_exception(self):
|
|
174
|
+
"""Test handling when no exception is active"""
|
|
175
|
+
with patch("sys.exc_info", return_value=(None, None, None)):
|
|
176
|
+
with self.assertRaises(RuntimeError) as cm:
|
|
177
|
+
self.engine.process_error()
|
|
178
|
+
self.assertIn("no active exception", str(cm.exception))
|
|
179
|
+
|
|
180
|
+
def test_get_error_failure(self):
|
|
181
|
+
"""Test handling when get_error fails"""
|
|
182
|
+
mock_exc = MockException("Test error")
|
|
183
|
+
|
|
184
|
+
# Mock get_error to raise an exception
|
|
185
|
+
with patch.object(
|
|
186
|
+
self.engine.sql, "get_error", side_effect=Exception("get_error failed")
|
|
187
|
+
):
|
|
188
|
+
with patch("sys.exc_info", return_value=(type(mock_exc), mock_exc, None)):
|
|
189
|
+
# Should still handle the error using fallback mechanisms
|
|
190
|
+
with self.assertRaises(
|
|
191
|
+
Exception
|
|
192
|
+
): # Original exception should be re-raised
|
|
193
|
+
self.engine.process_error()
|
|
194
|
+
|
|
195
|
+
def test_exception_str_failure(self):
|
|
196
|
+
"""Test handling when converting exception to string fails"""
|
|
197
|
+
|
|
198
|
+
class UnstringableException(Exception):
|
|
199
|
+
def __str__(self):
|
|
200
|
+
raise Exception("Cannot convert to string")
|
|
201
|
+
|
|
202
|
+
mock_exc = UnstringableException("Test error")
|
|
203
|
+
|
|
204
|
+
with patch("sys.exc_info", return_value=(type(mock_exc), mock_exc, None)):
|
|
205
|
+
with self.assertRaises(UnstringableException):
|
|
206
|
+
self.engine.process_error()
|
|
207
|
+
|
|
208
|
+
def test_exception_chaining(self):
|
|
209
|
+
"""Test that exception chaining is preserved"""
|
|
210
|
+
mock_exc = MockException("Original error", pgcode="23505")
|
|
211
|
+
|
|
212
|
+
with patch("sys.exc_info", return_value=(type(mock_exc), mock_exc, None)):
|
|
213
|
+
try:
|
|
214
|
+
self.engine.process_error()
|
|
215
|
+
except exceptions.DbDuplicateKeyError as e:
|
|
216
|
+
# Check that the original exception is chained
|
|
217
|
+
self.assertIsInstance(e.__cause__, MockException)
|
|
218
|
+
self.assertEqual(str(e.__cause__), "Original error")
|
|
219
|
+
|
|
220
|
+
def test_enhanced_logging(self):
|
|
221
|
+
"""Test that enhanced logging provides good context"""
|
|
222
|
+
mock_exc = MockException(
|
|
223
|
+
"Test error for logging", pgcode="23505", pgerror="duplicate key"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
with patch("sys.exc_info", return_value=(type(mock_exc), mock_exc, None)):
|
|
227
|
+
with patch("velocity.db.core.engine.logger") as mock_logger:
|
|
228
|
+
with self.assertRaises(exceptions.DbDuplicateKeyError):
|
|
229
|
+
self.engine.process_error("SELECT * FROM test", {"id": 123})
|
|
230
|
+
|
|
231
|
+
# Verify warning log was called with proper context
|
|
232
|
+
mock_logger.warning.assert_called_once()
|
|
233
|
+
call_args = mock_logger.warning.call_args
|
|
234
|
+
|
|
235
|
+
# Check the message contains key information
|
|
236
|
+
message = call_args[0][0]
|
|
237
|
+
self.assertIn("code=23505", message)
|
|
238
|
+
self.assertIn("message=duplicate key", message)
|
|
239
|
+
self.assertIn("type=MockException", message)
|
|
240
|
+
|
|
241
|
+
# Check extra context is provided
|
|
242
|
+
extra = call_args[1]["extra"]
|
|
243
|
+
self.assertEqual(extra["error_code"], "23505")
|
|
244
|
+
self.assertEqual(extra["sql_stmt"], "SELECT * FROM test")
|
|
245
|
+
self.assertEqual(extra["sql_params"], {"id": 123})
|
|
246
|
+
|
|
247
|
+
def test_unknown_error_logging(self):
|
|
248
|
+
"""Test logging for unhandled/unknown errors"""
|
|
249
|
+
|
|
250
|
+
class UnknownException(Exception):
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
mock_exc = UnknownException("Unknown error type")
|
|
254
|
+
|
|
255
|
+
with patch("sys.exc_info", return_value=(type(mock_exc), mock_exc, None)):
|
|
256
|
+
with patch("velocity.db.core.engine.logger") as mock_logger:
|
|
257
|
+
with self.assertRaises(UnknownException):
|
|
258
|
+
self.engine.process_error("SELECT unknown", {"param": "test"})
|
|
259
|
+
|
|
260
|
+
# Verify error log was called for unhandled case
|
|
261
|
+
mock_logger.error.assert_called_once()
|
|
262
|
+
call_args = mock_logger.error.call_args
|
|
263
|
+
|
|
264
|
+
# Check that comprehensive context is logged
|
|
265
|
+
extra = call_args[1]["extra"]
|
|
266
|
+
self.assertIn("available_error_codes", extra)
|
|
267
|
+
self.assertIn("original_exception_type", extra)
|
|
268
|
+
self.assertEqual(extra["original_exception_type"], "UnknownException")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main():
|
|
272
|
+
print("Testing robustness of improved process_error method...")
|
|
273
|
+
|
|
274
|
+
# Configure logging to see the output
|
|
275
|
+
logging.basicConfig(
|
|
276
|
+
level=logging.DEBUG, format="%(levelname)s - %(name)s - %(message)s"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Run the tests
|
|
280
|
+
unittest.main(argv=[""], exit=False, verbosity=2)
|
|
281
|
+
|
|
282
|
+
print("\n=== Summary ===")
|
|
283
|
+
print("✅ Enhanced error code classification")
|
|
284
|
+
print("✅ Robust regex pattern fallback")
|
|
285
|
+
print("✅ Exception chaining preservation")
|
|
286
|
+
print("✅ Enhanced logging with context")
|
|
287
|
+
print("✅ Graceful handling of edge cases")
|
|
288
|
+
print("✅ Better debugging information")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
main()
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test cases for Result class caching functionality.
|
|
4
|
+
|
|
5
|
+
Tests the new caching behavior in Result class that pre-fetches the first row
|
|
6
|
+
to enable immediate boolean evaluation and accurate state tracking.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import unittest
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
# Add src to path for imports
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
15
|
+
|
|
16
|
+
from velocity.db.core.result import Result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MockCursor:
|
|
20
|
+
"""Mock cursor for testing"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, rows=None, description=None, raise_on_fetch=False):
|
|
23
|
+
self.rows = rows or []
|
|
24
|
+
self.description = description or [("id",), ("name",)]
|
|
25
|
+
self.position = 0
|
|
26
|
+
self.raise_on_fetch = raise_on_fetch
|
|
27
|
+
self.closed = False
|
|
28
|
+
|
|
29
|
+
def fetchone(self):
|
|
30
|
+
if self.closed:
|
|
31
|
+
raise Exception("Cursor is closed")
|
|
32
|
+
if self.raise_on_fetch:
|
|
33
|
+
raise Exception("Simulated cursor error")
|
|
34
|
+
if self.position < len(self.rows):
|
|
35
|
+
row = self.rows[self.position]
|
|
36
|
+
self.position += 1
|
|
37
|
+
return row
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def fetchall(self):
|
|
41
|
+
if self.closed:
|
|
42
|
+
raise Exception("Cursor is closed")
|
|
43
|
+
remaining = self.rows[self.position :]
|
|
44
|
+
self.position = len(self.rows)
|
|
45
|
+
return remaining
|
|
46
|
+
|
|
47
|
+
def close(self):
|
|
48
|
+
self.closed = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestResultCaching(unittest.TestCase):
|
|
52
|
+
|
|
53
|
+
def test_empty_result(self):
|
|
54
|
+
"""Test with no rows"""
|
|
55
|
+
cursor = MockCursor(rows=[])
|
|
56
|
+
result = Result(cursor)
|
|
57
|
+
|
|
58
|
+
self.assertFalse(bool(result))
|
|
59
|
+
self.assertTrue(result.is_empty())
|
|
60
|
+
self.assertFalse(result.has_results())
|
|
61
|
+
self.assertIsNone(result.one())
|
|
62
|
+
|
|
63
|
+
def test_single_row(self):
|
|
64
|
+
"""Test with one row"""
|
|
65
|
+
cursor = MockCursor(rows=[(1, "Alice")])
|
|
66
|
+
result = Result(cursor)
|
|
67
|
+
|
|
68
|
+
self.assertTrue(bool(result))
|
|
69
|
+
self.assertFalse(result.is_empty())
|
|
70
|
+
self.assertTrue(result.has_results())
|
|
71
|
+
|
|
72
|
+
row = result.one()
|
|
73
|
+
self.assertEqual(row["id"], 1)
|
|
74
|
+
self.assertEqual(row["name"], "Alice")
|
|
75
|
+
|
|
76
|
+
def test_multiple_rows_boolean_state(self):
|
|
77
|
+
"""Test that boolean state changes as rows are consumed"""
|
|
78
|
+
cursor = MockCursor(rows=[(1, "Alice"), (2, "Bob"), (3, "Charlie")])
|
|
79
|
+
result = Result(cursor)
|
|
80
|
+
|
|
81
|
+
# Initially should be True (has results)
|
|
82
|
+
self.assertTrue(bool(result))
|
|
83
|
+
self.assertTrue(result.has_results())
|
|
84
|
+
self.assertFalse(result.is_empty())
|
|
85
|
+
|
|
86
|
+
# Consume first row
|
|
87
|
+
row1 = next(result)
|
|
88
|
+
self.assertEqual(row1["id"], 1)
|
|
89
|
+
|
|
90
|
+
# Should still be True (more rows available)
|
|
91
|
+
self.assertTrue(bool(result))
|
|
92
|
+
self.assertTrue(result.has_results())
|
|
93
|
+
|
|
94
|
+
# Consume second row
|
|
95
|
+
row2 = next(result)
|
|
96
|
+
self.assertEqual(row2["id"], 2)
|
|
97
|
+
|
|
98
|
+
# Should still be True (one more row available)
|
|
99
|
+
self.assertTrue(bool(result))
|
|
100
|
+
self.assertTrue(result.has_results())
|
|
101
|
+
|
|
102
|
+
# Consume third row
|
|
103
|
+
row3 = next(result)
|
|
104
|
+
self.assertEqual(row3["id"], 3)
|
|
105
|
+
|
|
106
|
+
# Now should be False (no more rows)
|
|
107
|
+
self.assertFalse(bool(result))
|
|
108
|
+
self.assertFalse(result.has_results())
|
|
109
|
+
self.assertTrue(result.is_empty())
|
|
110
|
+
|
|
111
|
+
# Trying to get another row should raise StopIteration
|
|
112
|
+
with self.assertRaises(StopIteration):
|
|
113
|
+
next(result)
|
|
114
|
+
|
|
115
|
+
def test_scalar_functionality(self):
|
|
116
|
+
"""Test scalar functionality"""
|
|
117
|
+
cursor = MockCursor(rows=[(42, "Answer")])
|
|
118
|
+
result = Result(cursor)
|
|
119
|
+
|
|
120
|
+
self.assertTrue(bool(result))
|
|
121
|
+
scalar_value = result.scalar()
|
|
122
|
+
self.assertEqual(scalar_value, 42)
|
|
123
|
+
|
|
124
|
+
# After scalar(), should be exhausted
|
|
125
|
+
self.assertFalse(bool(result))
|
|
126
|
+
|
|
127
|
+
def test_boolean_check_then_iterate(self):
|
|
128
|
+
"""Test checking boolean state then iterating"""
|
|
129
|
+
cursor = MockCursor(rows=[(1, "Alice"), (2, "Bob")])
|
|
130
|
+
result = Result(cursor)
|
|
131
|
+
|
|
132
|
+
# Check if we have results
|
|
133
|
+
if result:
|
|
134
|
+
rows = list(result)
|
|
135
|
+
self.assertEqual(len(rows), 2)
|
|
136
|
+
self.assertEqual(rows[0]["id"], 1)
|
|
137
|
+
self.assertEqual(rows[1]["id"], 2)
|
|
138
|
+
else:
|
|
139
|
+
self.fail("Result should have data")
|
|
140
|
+
|
|
141
|
+
# After consuming all, should be False
|
|
142
|
+
self.assertFalse(bool(result))
|
|
143
|
+
|
|
144
|
+
def test_one_method_exhausts_result(self):
|
|
145
|
+
"""Test that one() method marks result as exhausted"""
|
|
146
|
+
cursor = MockCursor(rows=[(1, "Alice"), (2, "Bob")])
|
|
147
|
+
result = Result(cursor)
|
|
148
|
+
|
|
149
|
+
self.assertTrue(bool(result))
|
|
150
|
+
|
|
151
|
+
row = result.one()
|
|
152
|
+
self.assertEqual(row["id"], 1)
|
|
153
|
+
|
|
154
|
+
# After one(), should be exhausted even though there were more rows
|
|
155
|
+
self.assertFalse(bool(result))
|
|
156
|
+
|
|
157
|
+
def test_caching_preserves_first_row(self):
|
|
158
|
+
"""Test that first row caching doesn't interfere with normal iteration"""
|
|
159
|
+
cursor = MockCursor(rows=[(1, "Alice"), (2, "Bob"), (3, "Charlie")])
|
|
160
|
+
result = Result(cursor)
|
|
161
|
+
|
|
162
|
+
# Check boolean state (which triggers first row caching)
|
|
163
|
+
self.assertTrue(bool(result))
|
|
164
|
+
|
|
165
|
+
# Iterate through all rows - should get all three
|
|
166
|
+
rows = list(result)
|
|
167
|
+
self.assertEqual(len(rows), 3)
|
|
168
|
+
self.assertEqual(rows[0]["id"], 1)
|
|
169
|
+
self.assertEqual(rows[1]["id"], 2)
|
|
170
|
+
self.assertEqual(rows[2]["id"], 3)
|
|
171
|
+
|
|
172
|
+
def test_multiple_boolean_checks(self):
|
|
173
|
+
"""Test multiple boolean checks return consistent results"""
|
|
174
|
+
cursor = MockCursor(rows=[(1, "Alice"), (2, "Bob")])
|
|
175
|
+
result = Result(cursor)
|
|
176
|
+
|
|
177
|
+
# Multiple boolean checks should be consistent
|
|
178
|
+
self.assertTrue(bool(result))
|
|
179
|
+
self.assertTrue(result.has_results())
|
|
180
|
+
self.assertFalse(result.is_empty())
|
|
181
|
+
self.assertTrue(bool(result)) # Should still be True
|
|
182
|
+
|
|
183
|
+
# Consume one row
|
|
184
|
+
next(result)
|
|
185
|
+
|
|
186
|
+
# Should still be True (one more row)
|
|
187
|
+
self.assertTrue(bool(result))
|
|
188
|
+
self.assertTrue(result.has_results())
|
|
189
|
+
|
|
190
|
+
# Consume last row
|
|
191
|
+
next(result)
|
|
192
|
+
|
|
193
|
+
# Now should be False
|
|
194
|
+
self.assertFalse(bool(result))
|
|
195
|
+
self.assertFalse(result.has_results())
|
|
196
|
+
self.assertTrue(result.is_empty())
|
|
197
|
+
|
|
198
|
+
def test_cursor_error_handling(self):
|
|
199
|
+
"""Test handling of cursor errors"""
|
|
200
|
+
cursor = MockCursor(rows=[(1, "Alice")], raise_on_fetch=True)
|
|
201
|
+
result = Result(cursor)
|
|
202
|
+
|
|
203
|
+
# Should handle cursor error gracefully and return False
|
|
204
|
+
self.assertFalse(bool(result))
|
|
205
|
+
self.assertTrue(result.is_empty())
|
|
206
|
+
self.assertFalse(result.has_results())
|
|
207
|
+
|
|
208
|
+
def test_closed_cursor_handling(self):
|
|
209
|
+
"""Test handling of operations on closed cursor"""
|
|
210
|
+
cursor = MockCursor(rows=[(1, "Alice"), (2, "Bob")])
|
|
211
|
+
result = Result(cursor)
|
|
212
|
+
|
|
213
|
+
# Should work initially
|
|
214
|
+
self.assertTrue(bool(result))
|
|
215
|
+
|
|
216
|
+
# Close the result explicitly
|
|
217
|
+
result.close()
|
|
218
|
+
|
|
219
|
+
# After closing, result should be exhausted
|
|
220
|
+
self.assertFalse(bool(result))
|
|
221
|
+
self.assertTrue(result.is_empty())
|
|
222
|
+
self.assertFalse(result.has_results())
|
|
223
|
+
|
|
224
|
+
def test_scalar_with_cursor_error(self):
|
|
225
|
+
"""Test scalar method with cursor errors"""
|
|
226
|
+
cursor = MockCursor(rows=[(42, "Answer")])
|
|
227
|
+
result = Result(cursor)
|
|
228
|
+
|
|
229
|
+
# Scalar should work with cached first row
|
|
230
|
+
scalar_value = result.scalar()
|
|
231
|
+
self.assertEqual(scalar_value, 42)
|
|
232
|
+
|
|
233
|
+
# Now test with a new result that has cursor error on fresh fetch
|
|
234
|
+
cursor2 = MockCursor(rows=[], raise_on_fetch=True)
|
|
235
|
+
result2 = Result(cursor2)
|
|
236
|
+
|
|
237
|
+
# Should return default value gracefully
|
|
238
|
+
scalar_value2 = result2.scalar("default")
|
|
239
|
+
self.assertEqual(scalar_value2, "default")
|
|
240
|
+
|
|
241
|
+
def test_columns_property_robustness(self):
|
|
242
|
+
"""Test columns property handles missing attributes gracefully"""
|
|
243
|
+
|
|
244
|
+
# Create a mock column that behaves like real DB cursor columns
|
|
245
|
+
class MockColumn:
|
|
246
|
+
def __init__(self, name):
|
|
247
|
+
self.name = name
|
|
248
|
+
|
|
249
|
+
cursor = MockCursor(rows=[(1, "test")])
|
|
250
|
+
cursor.description = [MockColumn("test_col")]
|
|
251
|
+
|
|
252
|
+
result = Result(cursor)
|
|
253
|
+
columns = result.columns
|
|
254
|
+
|
|
255
|
+
# Should have at least the column name
|
|
256
|
+
self.assertIn("test_col", columns)
|
|
257
|
+
self.assertIn("type_name", columns["test_col"])
|
|
258
|
+
self.assertEqual(columns["test_col"]["type_name"], "unknown")
|
|
259
|
+
|
|
260
|
+
def test_multiple_close_calls(self):
|
|
261
|
+
"""Test that multiple close calls don't cause issues"""
|
|
262
|
+
cursor = MockCursor(rows=[(1, "Alice")])
|
|
263
|
+
result = Result(cursor)
|
|
264
|
+
|
|
265
|
+
# Get the initial state (cached first row)
|
|
266
|
+
initial_state = bool(result)
|
|
267
|
+
self.assertTrue(initial_state)
|
|
268
|
+
|
|
269
|
+
# Multiple close calls should be safe
|
|
270
|
+
result.close()
|
|
271
|
+
result.close()
|
|
272
|
+
result.close()
|
|
273
|
+
|
|
274
|
+
# Result should be marked as exhausted after close
|
|
275
|
+
self.assertFalse(bool(result))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
unittest.main()
|