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.

Files changed (88) hide show
  1. velocity/__init__.py +1 -1
  2. velocity/app/tests/__init__.py +1 -0
  3. velocity/app/tests/test_email_processing.py +112 -0
  4. velocity/app/tests/test_payment_profile_sorting.py +191 -0
  5. velocity/app/tests/test_spreadsheet_functions.py +124 -0
  6. velocity/aws/tests/__init__.py +1 -0
  7. velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
  8. velocity/aws/tests/test_response.py +163 -0
  9. velocity/db/core/decorators.py +20 -3
  10. velocity/db/core/engine.py +33 -7
  11. velocity/db/exceptions.py +7 -0
  12. velocity/db/servers/base/__init__.py +9 -0
  13. velocity/db/servers/base/initializer.py +70 -0
  14. velocity/db/servers/base/operators.py +98 -0
  15. velocity/db/servers/base/sql.py +503 -0
  16. velocity/db/servers/base/types.py +135 -0
  17. velocity/db/servers/mysql/__init__.py +73 -0
  18. velocity/db/servers/mysql/operators.py +54 -0
  19. velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
  20. velocity/db/servers/mysql/sql.py +569 -0
  21. velocity/db/servers/mysql/types.py +107 -0
  22. velocity/db/servers/postgres/__init__.py +52 -2
  23. velocity/db/servers/postgres/operators.py +34 -0
  24. velocity/db/servers/postgres/sql.py +4 -3
  25. velocity/db/servers/postgres/types.py +88 -2
  26. velocity/db/servers/sqlite/__init__.py +61 -0
  27. velocity/db/servers/sqlite/operators.py +52 -0
  28. velocity/db/servers/sqlite/reserved.py +20 -0
  29. velocity/db/servers/sqlite/sql.py +530 -0
  30. velocity/db/servers/sqlite/types.py +92 -0
  31. velocity/db/servers/sqlserver/__init__.py +73 -0
  32. velocity/db/servers/sqlserver/operators.py +47 -0
  33. velocity/db/servers/sqlserver/reserved.py +32 -0
  34. velocity/db/servers/sqlserver/sql.py +625 -0
  35. velocity/db/servers/sqlserver/types.py +114 -0
  36. velocity/db/tests/__init__.py +1 -0
  37. velocity/db/tests/common_db_test.py +0 -0
  38. velocity/db/tests/postgres/__init__.py +1 -0
  39. velocity/db/tests/postgres/common.py +49 -0
  40. velocity/db/tests/postgres/test_column.py +29 -0
  41. velocity/db/tests/postgres/test_connections.py +25 -0
  42. velocity/db/tests/postgres/test_database.py +21 -0
  43. velocity/db/tests/postgres/test_engine.py +205 -0
  44. velocity/db/tests/postgres/test_general_usage.py +88 -0
  45. velocity/db/tests/postgres/test_imports.py +8 -0
  46. velocity/db/tests/postgres/test_result.py +19 -0
  47. velocity/db/tests/postgres/test_row.py +137 -0
  48. velocity/db/tests/postgres/test_schema_locking.py +335 -0
  49. velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
  50. velocity/db/tests/postgres/test_sequence.py +34 -0
  51. velocity/db/tests/postgres/test_table.py +101 -0
  52. velocity/db/tests/postgres/test_transaction.py +106 -0
  53. velocity/db/tests/sql/__init__.py +1 -0
  54. velocity/db/tests/sql/common.py +177 -0
  55. velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
  56. velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
  57. velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
  58. velocity/db/tests/test_db_utils.py +221 -0
  59. velocity/db/tests/test_postgres.py +212 -0
  60. velocity/db/tests/test_postgres_unchanged.py +81 -0
  61. velocity/db/tests/test_process_error_robustness.py +292 -0
  62. velocity/db/tests/test_result_caching.py +279 -0
  63. velocity/db/tests/test_result_sql_aware.py +117 -0
  64. velocity/db/tests/test_row_get_missing_column.py +72 -0
  65. velocity/db/tests/test_schema_locking_initializers.py +226 -0
  66. velocity/db/tests/test_schema_locking_simple.py +97 -0
  67. velocity/db/tests/test_sql_builder.py +165 -0
  68. velocity/db/tests/test_tablehelper.py +486 -0
  69. velocity/misc/tests/__init__.py +1 -0
  70. velocity/misc/tests/test_db.py +90 -0
  71. velocity/misc/tests/test_fix.py +78 -0
  72. velocity/misc/tests/test_format.py +64 -0
  73. velocity/misc/tests/test_iconv.py +203 -0
  74. velocity/misc/tests/test_merge.py +82 -0
  75. velocity/misc/tests/test_oconv.py +144 -0
  76. velocity/misc/tests/test_original_error.py +52 -0
  77. velocity/misc/tests/test_timer.py +74 -0
  78. {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/METADATA +1 -1
  79. velocity_python-0.0.134.dist-info/RECORD +125 -0
  80. velocity/db/servers/mysql.py +0 -640
  81. velocity/db/servers/sqlite.py +0 -968
  82. velocity/db/servers/sqlite_reserved.py +0 -208
  83. velocity/db/servers/sqlserver.py +0 -921
  84. velocity/db/servers/sqlserver_reserved.py +0 -314
  85. velocity_python-0.0.131.dist-info/RECORD +0 -62
  86. {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/WHEEL +0 -0
  87. {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/licenses/LICENSE +0 -0
  88. {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()