velocity-python 0.0.131__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/__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 +569 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +52 -2
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +4 -3
- 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 +530 -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 +625 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- 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.131.dist-info → velocity_python-0.0.134.dist-info}/METADATA +1 -1
- velocity_python-0.0.134.dist-info/RECORD +125 -0
- velocity/db/servers/mysql.py +0 -640
- 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.131.dist-info/RECORD +0 -62
- {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/top_level.txt +0 -0
|
@@ -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()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import Mock, MagicMock
|
|
3
|
+
from velocity.db.core.result import Result
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestResultSQLAwareFetch(unittest.TestCase):
|
|
7
|
+
"""
|
|
8
|
+
Test cases to verify that Result doesn't attempt to fetch from
|
|
9
|
+
INSERT/UPDATE/DELETE operations, preventing cursor errors.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def test_insert_sql_no_fetch_attempt(self):
|
|
13
|
+
"""Test that INSERT SQL doesn't attempt to fetch rows."""
|
|
14
|
+
mock_cursor = Mock()
|
|
15
|
+
|
|
16
|
+
# Create Result with INSERT SQL
|
|
17
|
+
result = Result(
|
|
18
|
+
cursor=mock_cursor, sql="INSERT INTO test (name) VALUES ('test')"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Verify fetchone was never called on INSERT
|
|
22
|
+
mock_cursor.fetchone.assert_not_called()
|
|
23
|
+
|
|
24
|
+
# Verify result is marked as exhausted (no rows expected)
|
|
25
|
+
self.assertTrue(result._exhausted)
|
|
26
|
+
self.assertTrue(result._first_row_fetched)
|
|
27
|
+
|
|
28
|
+
# Verify cursor is still valid (not set to None)
|
|
29
|
+
self.assertIsNotNone(result.cursor)
|
|
30
|
+
|
|
31
|
+
def test_update_sql_no_fetch_attempt(self):
|
|
32
|
+
"""Test that UPDATE SQL doesn't attempt to fetch rows."""
|
|
33
|
+
mock_cursor = Mock()
|
|
34
|
+
|
|
35
|
+
# Create Result with UPDATE SQL
|
|
36
|
+
result = Result(cursor=mock_cursor, sql="UPDATE test SET name='new' WHERE id=1")
|
|
37
|
+
|
|
38
|
+
# Verify fetchone was never called on UPDATE
|
|
39
|
+
mock_cursor.fetchone.assert_not_called()
|
|
40
|
+
|
|
41
|
+
# Verify result is marked as exhausted (no rows expected)
|
|
42
|
+
self.assertTrue(result._exhausted)
|
|
43
|
+
self.assertIsNotNone(result.cursor)
|
|
44
|
+
|
|
45
|
+
def test_delete_sql_no_fetch_attempt(self):
|
|
46
|
+
"""Test that DELETE SQL doesn't attempt to fetch rows."""
|
|
47
|
+
mock_cursor = Mock()
|
|
48
|
+
|
|
49
|
+
# Create Result with DELETE SQL
|
|
50
|
+
result = Result(cursor=mock_cursor, sql="DELETE FROM test WHERE id=1")
|
|
51
|
+
|
|
52
|
+
# Verify fetchone was never called on DELETE
|
|
53
|
+
mock_cursor.fetchone.assert_not_called()
|
|
54
|
+
|
|
55
|
+
# Verify result is marked as exhausted (no rows expected)
|
|
56
|
+
self.assertTrue(result._exhausted)
|
|
57
|
+
self.assertIsNotNone(result.cursor)
|
|
58
|
+
|
|
59
|
+
def test_select_sql_does_fetch(self):
|
|
60
|
+
"""Test that SELECT SQL still attempts to fetch rows."""
|
|
61
|
+
mock_cursor = Mock()
|
|
62
|
+
mock_cursor.fetchone.return_value = None # No rows returned
|
|
63
|
+
|
|
64
|
+
# Create Result with SELECT SQL
|
|
65
|
+
result = Result(cursor=mock_cursor, sql="SELECT * FROM test")
|
|
66
|
+
|
|
67
|
+
# Verify fetchone WAS called on SELECT
|
|
68
|
+
mock_cursor.fetchone.assert_called_once()
|
|
69
|
+
|
|
70
|
+
# Verify result is marked as exhausted (no rows returned)
|
|
71
|
+
self.assertTrue(result._exhausted)
|
|
72
|
+
self.assertIsNotNone(result.cursor)
|
|
73
|
+
|
|
74
|
+
def test_select_sql_with_rows(self):
|
|
75
|
+
"""Test that SELECT SQL with rows works correctly."""
|
|
76
|
+
mock_cursor = Mock()
|
|
77
|
+
mock_cursor.fetchone.return_value = ("test_value",)
|
|
78
|
+
mock_cursor.description = [("column1",)]
|
|
79
|
+
|
|
80
|
+
# Create Result with SELECT SQL
|
|
81
|
+
result = Result(cursor=mock_cursor, sql="SELECT column1 FROM test")
|
|
82
|
+
|
|
83
|
+
# Verify fetchone WAS called on SELECT
|
|
84
|
+
mock_cursor.fetchone.assert_called_once()
|
|
85
|
+
|
|
86
|
+
# Verify result has cached first row and is not exhausted
|
|
87
|
+
self.assertIsNotNone(result._cached_first_row)
|
|
88
|
+
self.assertFalse(result._exhausted)
|
|
89
|
+
self.assertIsNotNone(result.cursor)
|
|
90
|
+
|
|
91
|
+
def test_case_insensitive_sql_detection(self):
|
|
92
|
+
"""Test that SQL detection works with various cases."""
|
|
93
|
+
test_cases = [
|
|
94
|
+
"insert into test values (1)", # lowercase
|
|
95
|
+
"INSERT INTO test VALUES (1)", # uppercase
|
|
96
|
+
" INSERT INTO test VALUES (1)", # leading whitespace
|
|
97
|
+
"Insert Into test Values (1)", # mixed case
|
|
98
|
+
"UPDATE test SET name='x'", # update
|
|
99
|
+
"delete from test", # delete
|
|
100
|
+
"TRUNCATE TABLE test", # truncate
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for sql in test_cases:
|
|
104
|
+
with self.subTest(sql=sql):
|
|
105
|
+
mock_cursor = Mock()
|
|
106
|
+
result = Result(cursor=mock_cursor, sql=sql)
|
|
107
|
+
|
|
108
|
+
# Verify fetchone was never called
|
|
109
|
+
mock_cursor.fetchone.assert_not_called()
|
|
110
|
+
|
|
111
|
+
# Verify result is marked as exhausted
|
|
112
|
+
self.assertTrue(result._exhausted)
|
|
113
|
+
self.assertIsNotNone(result.cursor)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
unittest.main()
|