|
| 1 | +from dbt.adapters.snowflake.connections import SnowflakeConnectionManager |
| 2 | +from dbt.adapters.snowflake.record.cursor.cursor import SnowflakeRecordReplayCursor |
| 3 | + |
| 4 | + |
| 5 | +class MockStats: |
| 6 | + """Mock object that mimics snowflake-connector-python's QueryResultStats.""" |
| 7 | + |
| 8 | + def __init__( |
| 9 | + self, |
| 10 | + num_rows_inserted=100, |
| 11 | + num_rows_deleted=10, |
| 12 | + num_rows_updated=5, |
| 13 | + num_dml_duplicates=2, |
| 14 | + ): |
| 15 | + self.num_rows_inserted = num_rows_inserted |
| 16 | + self.num_rows_deleted = num_rows_deleted |
| 17 | + self.num_rows_updated = num_rows_updated |
| 18 | + self.num_dml_duplicates = num_dml_duplicates |
| 19 | + |
| 20 | + |
| 21 | +class MockCursor: |
| 22 | + """Mock cursor that mimics snowflake-connector-python's SnowflakeCursor.""" |
| 23 | + |
| 24 | + def __init__(self, stats=None): |
| 25 | + self._stats = stats |
| 26 | + |
| 27 | + @property |
| 28 | + def rowcount(self) -> int: |
| 29 | + return 42 |
| 30 | + |
| 31 | + @property |
| 32 | + def sqlstate(self) -> str: |
| 33 | + return "00000" |
| 34 | + |
| 35 | + @property |
| 36 | + def sfqid(self) -> str: |
| 37 | + return "01abc123-0001-abcd-0000-00012345abcd" |
| 38 | + |
| 39 | + @property |
| 40 | + def stats(self): |
| 41 | + return self._stats |
| 42 | + |
| 43 | + def execute(self, operation, parameters=None) -> None: |
| 44 | + pass |
| 45 | + |
| 46 | + @property |
| 47 | + def unexpected_prop(self) -> bool: |
| 48 | + return True |
| 49 | + |
| 50 | + def unexpected_func(self) -> int: |
| 51 | + return 1 |
| 52 | + |
| 53 | + |
| 54 | +class MockConnection: |
| 55 | + name = "test_connection" |
| 56 | + |
| 57 | + |
| 58 | +def test_snowflake_record_cursor_sqlstate(): |
| 59 | + """Test that the sqlstate property works correctly.""" |
| 60 | + recorded_cursor = SnowflakeRecordReplayCursor(MockCursor(), MockConnection()) # type: ignore |
| 61 | + assert recorded_cursor.sqlstate == "00000" |
| 62 | + |
| 63 | + |
| 64 | +def test_snowflake_record_cursor_sfqid(): |
| 65 | + """Test that the sfqid property works correctly.""" |
| 66 | + recorded_cursor = SnowflakeRecordReplayCursor(MockCursor(), MockConnection()) # type: ignore |
| 67 | + assert recorded_cursor.sfqid == "01abc123-0001-abcd-0000-00012345abcd" |
| 68 | + |
| 69 | + |
| 70 | +def test_snowflake_record_cursor_stats(): |
| 71 | + """Test that the stats property works correctly.""" |
| 72 | + mock_stats = MockStats() |
| 73 | + recorded_cursor = SnowflakeRecordReplayCursor( |
| 74 | + MockCursor(stats=mock_stats), MockConnection() |
| 75 | + ) # type: ignore |
| 76 | + |
| 77 | + stats = recorded_cursor.stats |
| 78 | + assert stats.num_rows_inserted == 100 |
| 79 | + assert stats.num_rows_deleted == 10 |
| 80 | + assert stats.num_rows_updated == 5 |
| 81 | + assert stats.num_dml_duplicates == 2 |
| 82 | + |
| 83 | + |
| 84 | +def test_snowflake_record_cursor_stats_none(): |
| 85 | + """Test that the stats property handles None correctly.""" |
| 86 | + recorded_cursor = SnowflakeRecordReplayCursor( |
| 87 | + MockCursor(stats=None), MockConnection() |
| 88 | + ) # type: ignore |
| 89 | + |
| 90 | + assert recorded_cursor.stats is None |
| 91 | + |
| 92 | + |
| 93 | +def test_snowflake_record_cursor_inherited_properties(): |
| 94 | + """Test that inherited properties from RecordReplayCursor work correctly.""" |
| 95 | + recorded_cursor = SnowflakeRecordReplayCursor(MockCursor(), MockConnection()) # type: ignore |
| 96 | + |
| 97 | + # Test inherited rowcount property |
| 98 | + assert recorded_cursor.rowcount == 42 |
| 99 | + |
| 100 | + # Test inherited execute method |
| 101 | + recorded_cursor.execute("SELECT 1") |
| 102 | + |
| 103 | + |
| 104 | +def test_snowflake_record_cursor_unexpected_access(): |
| 105 | + """Test that unexpected property/method access fires a warning but still works.""" |
| 106 | + recorded_cursor = SnowflakeRecordReplayCursor(MockCursor(), MockConnection()) # type: ignore |
| 107 | + |
| 108 | + events = [] |
| 109 | + # Mock event firing |
| 110 | + recorded_cursor._fire_event = events.append |
| 111 | + |
| 112 | + # Test that an unexpected property works, but fires a warning |
| 113 | + assert recorded_cursor.unexpected_prop is True |
| 114 | + assert len(events) == 1 |
| 115 | + assert events[0].__class__.__name__ == "RecordReplayIssue" |
| 116 | + assert "unexpected_prop" in events[0].msg |
| 117 | + events.clear() |
| 118 | + |
| 119 | + # Test that an unexpected function works, but fires a warning |
| 120 | + assert recorded_cursor.unexpected_func() == 1 |
| 121 | + assert len(events) == 1 |
| 122 | + assert events[0].__class__.__name__ == "RecordReplayIssue" |
| 123 | + assert "unexpected_func" in events[0].msg |
| 124 | + |
| 125 | + |
| 126 | +def test_get_response_no_unexpected_access_warnings(): |
| 127 | + """Ensure get_response() doesn't trigger any unexpected attribute access warnings. |
| 128 | +
|
| 129 | + This is a regression test. If new cursor attributes are accessed in get_response() |
| 130 | + without being added to SnowflakeRecordReplayCursor, this test will fail. |
| 131 | + """ |
| 132 | + events = [] |
| 133 | + |
| 134 | + # Test with stats present |
| 135 | + mock_cursor = MockCursor(stats=MockStats()) |
| 136 | + recorded_cursor = SnowflakeRecordReplayCursor(mock_cursor, MockConnection()) # type: ignore |
| 137 | + recorded_cursor._fire_event = events.append |
| 138 | + |
| 139 | + # Call get_response - this is the actual code path |
| 140 | + response = SnowflakeConnectionManager.get_response(recorded_cursor) |
| 141 | + |
| 142 | + # Verify no unexpected access warnings were fired |
| 143 | + assert len(events) == 0, ( |
| 144 | + f"Unexpected attribute access in get_response(): {[e.msg for e in events]}. " |
| 145 | + "Add the missing attribute(s) to SnowflakeRecordReplayCursor." |
| 146 | + ) |
| 147 | + |
| 148 | + # Verify the response was created successfully |
| 149 | + assert response is not None |
| 150 | + assert response.code == "00000" # SQL success state from mock cursor |
0 commit comments