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.
Files changed (120) hide show
  1. velocity/__init__.py +3 -1
  2. velocity/app/orders.py +3 -4
  3. velocity/app/tests/__init__.py +1 -0
  4. velocity/app/tests/test_email_processing.py +112 -0
  5. velocity/app/tests/test_payment_profile_sorting.py +191 -0
  6. velocity/app/tests/test_spreadsheet_functions.py +124 -0
  7. velocity/aws/__init__.py +3 -0
  8. velocity/aws/amplify.py +10 -6
  9. velocity/aws/handlers/__init__.py +2 -0
  10. velocity/aws/handlers/base_handler.py +248 -0
  11. velocity/aws/handlers/context.py +167 -2
  12. velocity/aws/handlers/exceptions.py +16 -0
  13. velocity/aws/handlers/lambda_handler.py +24 -85
  14. velocity/aws/handlers/mixins/__init__.py +16 -0
  15. velocity/aws/handlers/mixins/activity_tracker.py +181 -0
  16. velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
  17. velocity/aws/handlers/mixins/error_handler.py +192 -0
  18. velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
  19. velocity/aws/handlers/mixins/standard_mixin.py +73 -0
  20. velocity/aws/handlers/response.py +1 -1
  21. velocity/aws/handlers/sqs_handler.py +28 -143
  22. velocity/aws/tests/__init__.py +1 -0
  23. velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
  24. velocity/aws/tests/test_response.py +163 -0
  25. velocity/db/__init__.py +16 -4
  26. velocity/db/core/decorators.py +20 -4
  27. velocity/db/core/engine.py +185 -839
  28. velocity/db/core/result.py +30 -24
  29. velocity/db/core/row.py +15 -3
  30. velocity/db/core/table.py +279 -40
  31. velocity/db/core/transaction.py +19 -11
  32. velocity/db/exceptions.py +42 -18
  33. velocity/db/servers/base/__init__.py +9 -0
  34. velocity/db/servers/base/initializer.py +70 -0
  35. velocity/db/servers/base/operators.py +98 -0
  36. velocity/db/servers/base/sql.py +503 -0
  37. velocity/db/servers/base/types.py +135 -0
  38. velocity/db/servers/mysql/__init__.py +73 -0
  39. velocity/db/servers/mysql/operators.py +54 -0
  40. velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
  41. velocity/db/servers/mysql/sql.py +718 -0
  42. velocity/db/servers/mysql/types.py +107 -0
  43. velocity/db/servers/postgres/__init__.py +59 -11
  44. velocity/db/servers/postgres/operators.py +34 -0
  45. velocity/db/servers/postgres/sql.py +474 -120
  46. velocity/db/servers/postgres/types.py +88 -2
  47. velocity/db/servers/sqlite/__init__.py +61 -0
  48. velocity/db/servers/sqlite/operators.py +52 -0
  49. velocity/db/servers/sqlite/reserved.py +20 -0
  50. velocity/db/servers/sqlite/sql.py +677 -0
  51. velocity/db/servers/sqlite/types.py +92 -0
  52. velocity/db/servers/sqlserver/__init__.py +73 -0
  53. velocity/db/servers/sqlserver/operators.py +47 -0
  54. velocity/db/servers/sqlserver/reserved.py +32 -0
  55. velocity/db/servers/sqlserver/sql.py +805 -0
  56. velocity/db/servers/sqlserver/types.py +114 -0
  57. velocity/db/servers/tablehelper.py +117 -91
  58. velocity/db/tests/__init__.py +1 -0
  59. velocity/db/tests/common_db_test.py +0 -0
  60. velocity/db/tests/postgres/__init__.py +1 -0
  61. velocity/db/tests/postgres/common.py +49 -0
  62. velocity/db/tests/postgres/test_column.py +29 -0
  63. velocity/db/tests/postgres/test_connections.py +25 -0
  64. velocity/db/tests/postgres/test_database.py +21 -0
  65. velocity/db/tests/postgres/test_engine.py +205 -0
  66. velocity/db/tests/postgres/test_general_usage.py +88 -0
  67. velocity/db/tests/postgres/test_imports.py +8 -0
  68. velocity/db/tests/postgres/test_result.py +19 -0
  69. velocity/db/tests/postgres/test_row.py +137 -0
  70. velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
  71. velocity/db/tests/postgres/test_schema_locking.py +335 -0
  72. velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
  73. velocity/db/tests/postgres/test_sequence.py +34 -0
  74. velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
  75. velocity/db/tests/postgres/test_table.py +101 -0
  76. velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
  77. velocity/db/tests/postgres/test_transaction.py +106 -0
  78. velocity/db/tests/sql/__init__.py +1 -0
  79. velocity/db/tests/sql/common.py +177 -0
  80. velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
  81. velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
  82. velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
  83. velocity/db/tests/test_db_utils.py +221 -0
  84. velocity/db/tests/test_postgres.py +448 -0
  85. velocity/db/tests/test_postgres_unchanged.py +81 -0
  86. velocity/db/tests/test_process_error_robustness.py +292 -0
  87. velocity/db/tests/test_result_caching.py +279 -0
  88. velocity/db/tests/test_result_sql_aware.py +117 -0
  89. velocity/db/tests/test_row_get_missing_column.py +72 -0
  90. velocity/db/tests/test_schema_locking_initializers.py +226 -0
  91. velocity/db/tests/test_schema_locking_simple.py +97 -0
  92. velocity/db/tests/test_sql_builder.py +165 -0
  93. velocity/db/tests/test_tablehelper.py +486 -0
  94. velocity/db/utils.py +62 -47
  95. velocity/misc/conv/__init__.py +2 -0
  96. velocity/misc/conv/iconv.py +5 -4
  97. velocity/misc/export.py +1 -4
  98. velocity/misc/merge.py +1 -1
  99. velocity/misc/tests/__init__.py +1 -0
  100. velocity/misc/tests/test_db.py +90 -0
  101. velocity/misc/tests/test_fix.py +78 -0
  102. velocity/misc/tests/test_format.py +64 -0
  103. velocity/misc/tests/test_iconv.py +203 -0
  104. velocity/misc/tests/test_merge.py +82 -0
  105. velocity/misc/tests/test_oconv.py +144 -0
  106. velocity/misc/tests/test_original_error.py +52 -0
  107. velocity/misc/tests/test_timer.py +74 -0
  108. velocity/misc/tools.py +0 -1
  109. {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/METADATA +2 -2
  110. velocity_python-0.0.155.dist-info/RECORD +129 -0
  111. velocity/db/core/exceptions.py +0 -70
  112. velocity/db/servers/mysql.py +0 -641
  113. velocity/db/servers/sqlite.py +0 -968
  114. velocity/db/servers/sqlite_reserved.py +0 -208
  115. velocity/db/servers/sqlserver.py +0 -921
  116. velocity/db/servers/sqlserver_reserved.py +0 -314
  117. velocity_python-0.0.109.dist-info/RECORD +0 -56
  118. {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
  119. {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
  120. {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()