velocity-python 0.0.105__tar.gz → 0.0.108__tar.gz

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 (82) hide show
  1. {velocity_python-0.0.105/src/velocity_python.egg-info → velocity_python-0.0.108}/PKG-INFO +1 -1
  2. {velocity_python-0.0.105 → velocity_python-0.0.108}/pyproject.toml +1 -1
  3. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/result.py +8 -0
  5. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/table.py +4 -4
  6. {velocity_python-0.0.105 → velocity_python-0.0.108/src/velocity_python.egg-info}/PKG-INFO +1 -1
  7. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  8. velocity_python-0.0.108/tests/test_cursor_rowcount_fix.py +150 -0
  9. velocity_python-0.0.108/tests/test_result_sql_aware.py +115 -0
  10. {velocity_python-0.0.105 → velocity_python-0.0.108}/LICENSE +0 -0
  11. {velocity_python-0.0.105 → velocity_python-0.0.108}/README.md +0 -0
  12. {velocity_python-0.0.105 → velocity_python-0.0.108}/setup.cfg +0 -0
  13. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/app/__init__.py +0 -0
  14. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/app/invoices.py +0 -0
  15. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/app/orders.py +0 -0
  16. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/app/payments.py +0 -0
  17. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/app/purchase_orders.py +0 -0
  18. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/aws/__init__.py +0 -0
  19. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/aws/amplify.py +0 -0
  20. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/aws/handlers/__init__.py +0 -0
  21. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/aws/handlers/context.py +0 -0
  22. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  23. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/aws/handlers/response.py +0 -0
  24. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  25. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/__init__.py +0 -0
  26. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/__init__.py +0 -0
  27. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/column.py +0 -0
  28. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/database.py +0 -0
  29. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/decorators.py +0 -0
  30. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/engine.py +0 -0
  31. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/exceptions.py +0 -0
  32. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/row.py +0 -0
  33. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/sequence.py +0 -0
  34. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/core/transaction.py +0 -0
  35. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/exceptions.py +0 -0
  36. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/__init__.py +0 -0
  37. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/mysql.py +0 -0
  38. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/mysql_reserved.py +0 -0
  39. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/postgres/__init__.py +0 -0
  40. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/postgres/operators.py +0 -0
  41. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/postgres/reserved.py +0 -0
  42. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/postgres/sql.py +0 -0
  43. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/postgres/types.py +0 -0
  44. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/sqlite.py +0 -0
  45. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/sqlite_reserved.py +0 -0
  46. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/sqlserver.py +0 -0
  47. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/sqlserver_reserved.py +0 -0
  48. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/servers/tablehelper.py +0 -0
  49. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/db/utils.py +0 -0
  50. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/__init__.py +0 -0
  51. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/conv/__init__.py +0 -0
  52. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/conv/iconv.py +0 -0
  53. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/conv/oconv.py +0 -0
  54. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/db.py +0 -0
  55. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/export.py +0 -0
  56. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/format.py +0 -0
  57. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/mail.py +0 -0
  58. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/merge.py +0 -0
  59. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/timer.py +0 -0
  60. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity/misc/tools.py +0 -0
  61. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  62. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity_python.egg-info/requires.txt +0 -0
  63. {velocity_python-0.0.105 → velocity_python-0.0.108}/src/velocity_python.egg-info/top_level.txt +0 -0
  64. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_db.py +0 -0
  65. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_db_utils.py +0 -0
  66. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_email_processing.py +0 -0
  67. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_fix.py +0 -0
  68. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_format.py +0 -0
  69. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_iconv.py +0 -0
  70. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_merge.py +0 -0
  71. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_oconv.py +0 -0
  72. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_original_error.py +0 -0
  73. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_payment_profile_sorting.py +0 -0
  74. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_postgres.py +0 -0
  75. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_process_error_robustness.py +0 -0
  76. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_response.py +0 -0
  77. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_result_caching.py +0 -0
  78. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_row_get_missing_column.py +0 -0
  79. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_spreadsheet_functions.py +0 -0
  80. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_sql_builder.py +0 -0
  81. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_tablehelper.py +0 -0
  82. {velocity_python-0.0.105 → velocity_python-0.0.108}/tests/test_timer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.105
3
+ Version: 0.0.108
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.105"
7
+ version = "0.0.108"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.98"
1
+ __version__ = version = "0.0.108"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -56,10 +56,18 @@ class Result:
56
56
  def _fetch_first_row(self):
57
57
  """
58
58
  Pre-fetch the first row from the cursor to enable immediate boolean evaluation.
59
+ Only attempts to fetch for SELECT-like operations that return rows.
59
60
  """
60
61
  if self._first_row_fetched or not self._cursor:
61
62
  return
62
63
 
64
+ # Don't try to fetch from INSERT/UPDATE/DELETE operations
65
+ # These operations don't return rows, only rowcount
66
+ if self.__sql and self.__sql.strip().upper().startswith(('INSERT', 'UPDATE', 'DELETE', 'TRUNCATE')):
67
+ self._exhausted = True
68
+ self._first_row_fetched = True
69
+ return
70
+
63
71
  try:
64
72
  raw_row = self._cursor.fetchone()
65
73
  if raw_row:
@@ -431,7 +431,7 @@ class Table:
431
431
  if kwds.get("sql_only", False):
432
432
  return sql, vals
433
433
  result = self.tx.execute(sql, vals, cursor=self.cursor())
434
- return result.cursor.rowcount
434
+ return result.cursor.rowcount if result.cursor else 0
435
435
 
436
436
  @reset_id_on_dup_key
437
437
  @create_missing
@@ -443,7 +443,7 @@ class Table:
443
443
  if kwds.get("sql_only", False):
444
444
  return sql, vals
445
445
  result = self.tx.execute(sql, vals, cursor=self.cursor())
446
- return result.cursor.rowcount
446
+ return result.cursor.rowcount if result.cursor else 0
447
447
 
448
448
  @reset_id_on_dup_key
449
449
  @create_missing
@@ -462,7 +462,7 @@ class Table:
462
462
  if kwds.get("sql_only", False):
463
463
  return sql, vals
464
464
  result = self.tx.execute(sql, vals, cursor=self.cursor())
465
- return result.cursor.rowcount
465
+ return result.cursor.rowcount if result.cursor else 0
466
466
 
467
467
  upsert = merge
468
468
  indate = merge
@@ -662,7 +662,7 @@ class Table:
662
662
  if kwds.get("sql_only", False):
663
663
  return sql, vals
664
664
  result = self.tx.execute(sql, vals)
665
- return result.cursor.rowcount
665
+ return result.cursor.rowcount if result.cursor else 0
666
666
 
667
667
  def truncate(self, **kwds):
668
668
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.105
3
+ Version: 0.0.108
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -57,6 +57,7 @@ src/velocity_python.egg-info/SOURCES.txt
57
57
  src/velocity_python.egg-info/dependency_links.txt
58
58
  src/velocity_python.egg-info/requires.txt
59
59
  src/velocity_python.egg-info/top_level.txt
60
+ tests/test_cursor_rowcount_fix.py
60
61
  tests/test_db.py
61
62
  tests/test_db_utils.py
62
63
  tests/test_email_processing.py
@@ -71,6 +72,7 @@ tests/test_postgres.py
71
72
  tests/test_process_error_robustness.py
72
73
  tests/test_response.py
73
74
  tests/test_result_caching.py
75
+ tests/test_result_sql_aware.py
74
76
  tests/test_row_get_missing_column.py
75
77
  tests/test_spreadsheet_functions.py
76
78
  tests/test_sql_builder.py
@@ -0,0 +1,150 @@
1
+ import unittest
2
+ from unittest.mock import Mock, MagicMock
3
+ from velocity.db.core.table import Table
4
+ from velocity.db.core.result import Result
5
+
6
+
7
+ class TestCursorRowCountFix(unittest.TestCase):
8
+ """
9
+ Test cases to verify that table methods handle None cursors gracefully
10
+ when accessing result.cursor.rowcount.
11
+
12
+ This addresses the AttributeError: 'NoneType' object has no attribute 'rowcount'
13
+ issue that can occur when the cursor is None due to connection errors or
14
+ other exceptional conditions.
15
+ """
16
+
17
+ def setUp(self):
18
+ """Set up mock objects for testing."""
19
+ self.mock_tx = Mock()
20
+ self.mock_sql = Mock()
21
+ self.table = Table(self.mock_tx, "test_table")
22
+ self.table.sql = self.mock_sql
23
+
24
+ def test_insert_with_none_cursor(self):
25
+ """Test that insert() returns 0 when result.cursor is None."""
26
+ # Mock the SQL generation
27
+ self.mock_sql.insert.return_value = ("INSERT SQL", ["values"])
28
+
29
+ # Create a mock result with cursor = None
30
+ mock_result = Mock(spec=Result)
31
+ mock_result.cursor = None
32
+
33
+ # Mock the execute method to return our mock result
34
+ self.mock_tx.execute.return_value = mock_result
35
+
36
+ # Mock the cursor method
37
+ mock_cursor = Mock()
38
+ self.table.cursor = Mock(return_value=mock_cursor)
39
+
40
+ # Call insert and verify it returns 0 instead of raising AttributeError
41
+ result = self.table.insert({"test_field": "test_value"})
42
+ self.assertEqual(result, 0)
43
+
44
+ def test_update_with_none_cursor(self):
45
+ """Test that update() returns 0 when result.cursor is None."""
46
+ # Mock the SQL generation
47
+ self.mock_sql.update.return_value = ("UPDATE SQL", ["values"])
48
+
49
+ # Create a mock result with cursor = None
50
+ mock_result = Mock(spec=Result)
51
+ mock_result.cursor = None
52
+
53
+ # Mock the execute method to return our mock result
54
+ self.mock_tx.execute.return_value = mock_result
55
+
56
+ # Mock the cursor method
57
+ mock_cursor = Mock()
58
+ self.table.cursor = Mock(return_value=mock_cursor)
59
+
60
+ # Call update and verify it returns 0 instead of raising AttributeError
61
+ result = self.table.update({"test_field": "new_value"}, where={"id": 1})
62
+ self.assertEqual(result, 0)
63
+
64
+ def test_merge_with_none_cursor(self):
65
+ """Test that merge() returns 0 when result.cursor is None."""
66
+ # Mock the SQL generation
67
+ self.mock_sql.merge.return_value = ("MERGE SQL", ["values"])
68
+
69
+ # Create a mock result with cursor = None
70
+ mock_result = Mock(spec=Result)
71
+ mock_result.cursor = None
72
+
73
+ # Mock the execute method to return our mock result
74
+ self.mock_tx.execute.return_value = mock_result
75
+
76
+ # Mock the cursor method
77
+ mock_cursor = Mock()
78
+ self.table.cursor = Mock(return_value=mock_cursor)
79
+
80
+ # Call merge and verify it returns 0 instead of raising AttributeError
81
+ result = self.table.merge({"test_field": "test_value"})
82
+ self.assertEqual(result, 0)
83
+
84
+ def test_delete_with_none_cursor(self):
85
+ """Test that delete() returns 0 when result.cursor is None."""
86
+ # Mock the SQL generation
87
+ self.mock_sql.delete.return_value = ("DELETE SQL", ["values"])
88
+
89
+ # Create a mock result with cursor = None
90
+ mock_result = Mock(spec=Result)
91
+ mock_result.cursor = None
92
+
93
+ # Mock the execute method to return our mock result
94
+ self.mock_tx.execute.return_value = mock_result
95
+
96
+ # Call delete and verify it returns 0 instead of raising AttributeError
97
+ result = self.table.delete(where={"id": 1})
98
+ self.assertEqual(result, 0)
99
+
100
+ def test_insert_with_valid_cursor(self):
101
+ """Test that insert() returns rowcount when result.cursor is valid."""
102
+ # Mock the SQL generation
103
+ self.mock_sql.insert.return_value = ("INSERT SQL", ["values"])
104
+
105
+ # Create a mock cursor with rowcount
106
+ mock_cursor = Mock()
107
+ mock_cursor.rowcount = 1
108
+
109
+ # Create a mock result with valid cursor
110
+ mock_result = Mock(spec=Result)
111
+ mock_result.cursor = mock_cursor
112
+
113
+ # Mock the execute method to return our mock result
114
+ self.mock_tx.execute.return_value = mock_result
115
+
116
+ # Mock the cursor method
117
+ table_cursor = Mock()
118
+ self.table.cursor = Mock(return_value=table_cursor)
119
+
120
+ # Call insert and verify it returns the actual rowcount
121
+ result = self.table.insert({"test_field": "test_value"})
122
+ self.assertEqual(result, 1)
123
+
124
+ def test_update_with_valid_cursor(self):
125
+ """Test that update() returns rowcount when result.cursor is valid."""
126
+ # Mock the SQL generation
127
+ self.mock_sql.update.return_value = ("UPDATE SQL", ["values"])
128
+
129
+ # Create a mock cursor with rowcount
130
+ mock_cursor = Mock()
131
+ mock_cursor.rowcount = 2
132
+
133
+ # Create a mock result with valid cursor
134
+ mock_result = Mock(spec=Result)
135
+ mock_result.cursor = mock_cursor
136
+
137
+ # Mock the execute method to return our mock result
138
+ self.mock_tx.execute.return_value = mock_result
139
+
140
+ # Mock the cursor method
141
+ table_cursor = Mock()
142
+ self.table.cursor = Mock(return_value=table_cursor)
143
+
144
+ # Call update and verify it returns the actual rowcount
145
+ result = self.table.update({"test_field": "new_value"}, where={"id": 1})
146
+ self.assertEqual(result, 2)
147
+
148
+
149
+ if __name__ == "__main__":
150
+ unittest.main()
@@ -0,0 +1,115 @@
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(cursor=mock_cursor, sql="INSERT INTO test (name) VALUES ('test')")
18
+
19
+ # Verify fetchone was never called on INSERT
20
+ mock_cursor.fetchone.assert_not_called()
21
+
22
+ # Verify result is marked as exhausted (no rows expected)
23
+ self.assertTrue(result._exhausted)
24
+ self.assertTrue(result._first_row_fetched)
25
+
26
+ # Verify cursor is still valid (not set to None)
27
+ self.assertIsNotNone(result.cursor)
28
+
29
+ def test_update_sql_no_fetch_attempt(self):
30
+ """Test that UPDATE SQL doesn't attempt to fetch rows."""
31
+ mock_cursor = Mock()
32
+
33
+ # Create Result with UPDATE SQL
34
+ result = Result(cursor=mock_cursor, sql="UPDATE test SET name='new' WHERE id=1")
35
+
36
+ # Verify fetchone was never called on UPDATE
37
+ mock_cursor.fetchone.assert_not_called()
38
+
39
+ # Verify result is marked as exhausted (no rows expected)
40
+ self.assertTrue(result._exhausted)
41
+ self.assertIsNotNone(result.cursor)
42
+
43
+ def test_delete_sql_no_fetch_attempt(self):
44
+ """Test that DELETE SQL doesn't attempt to fetch rows."""
45
+ mock_cursor = Mock()
46
+
47
+ # Create Result with DELETE SQL
48
+ result = Result(cursor=mock_cursor, sql="DELETE FROM test WHERE id=1")
49
+
50
+ # Verify fetchone was never called on DELETE
51
+ mock_cursor.fetchone.assert_not_called()
52
+
53
+ # Verify result is marked as exhausted (no rows expected)
54
+ self.assertTrue(result._exhausted)
55
+ self.assertIsNotNone(result.cursor)
56
+
57
+ def test_select_sql_does_fetch(self):
58
+ """Test that SELECT SQL still attempts to fetch rows."""
59
+ mock_cursor = Mock()
60
+ mock_cursor.fetchone.return_value = None # No rows returned
61
+
62
+ # Create Result with SELECT SQL
63
+ result = Result(cursor=mock_cursor, sql="SELECT * FROM test")
64
+
65
+ # Verify fetchone WAS called on SELECT
66
+ mock_cursor.fetchone.assert_called_once()
67
+
68
+ # Verify result is marked as exhausted (no rows returned)
69
+ self.assertTrue(result._exhausted)
70
+ self.assertIsNotNone(result.cursor)
71
+
72
+ def test_select_sql_with_rows(self):
73
+ """Test that SELECT SQL with rows works correctly."""
74
+ mock_cursor = Mock()
75
+ mock_cursor.fetchone.return_value = ('test_value',)
76
+ mock_cursor.description = [('column1',)]
77
+
78
+ # Create Result with SELECT SQL
79
+ result = Result(cursor=mock_cursor, sql="SELECT column1 FROM test")
80
+
81
+ # Verify fetchone WAS called on SELECT
82
+ mock_cursor.fetchone.assert_called_once()
83
+
84
+ # Verify result has cached first row and is not exhausted
85
+ self.assertIsNotNone(result._cached_first_row)
86
+ self.assertFalse(result._exhausted)
87
+ self.assertIsNotNone(result.cursor)
88
+
89
+ def test_case_insensitive_sql_detection(self):
90
+ """Test that SQL detection works with various cases."""
91
+ test_cases = [
92
+ "insert into test values (1)", # lowercase
93
+ "INSERT INTO test VALUES (1)", # uppercase
94
+ " INSERT INTO test VALUES (1)", # leading whitespace
95
+ "Insert Into test Values (1)", # mixed case
96
+ "UPDATE test SET name='x'", # update
97
+ "delete from test", # delete
98
+ "TRUNCATE TABLE test" # truncate
99
+ ]
100
+
101
+ for sql in test_cases:
102
+ with self.subTest(sql=sql):
103
+ mock_cursor = Mock()
104
+ result = Result(cursor=mock_cursor, sql=sql)
105
+
106
+ # Verify fetchone was never called
107
+ mock_cursor.fetchone.assert_not_called()
108
+
109
+ # Verify result is marked as exhausted
110
+ self.assertTrue(result._exhausted)
111
+ self.assertIsNotNone(result.cursor)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()