Rajiv Soundararajan

83 papers A* 7A 6B 8Misc 3Journal 50Unranked 8
YearRankTypeTitle / Venue / Authors
2026 J jnl
IEEE Trans. Image Process.
Pengfei Chen, Jiebin Yan, Rajiv Soundararajan, Giuseppe Valenzise, Li Cai, Leida Li
2025 A* conf
SIGGRAPH Asia
Gurutva Patle, Nilay Girgaonkar, Nagabhushan Somraj, Rajiv Soundararajan
2025 J jnl
CoRR
Gurutva Patle, Nilay Girgaonkar, Nagabhushan Somraj, Rajiv Soundararajan
2025 J jnl
CoRR
Nithin C. Babu, Aniruddha Mahapatra, Harsh Rangwani, Rajiv Soundararajan, Kuldeep Kulkarni
2025 J jnl
Trans. Mach. Learn. Res.
Shankhanil Mitra, Diptanu De, Shika Rao, Rajiv Soundararajan
2025 J jnl
IEEE Trans. Geosci. Remote. Sens.
Aakanksha Bharti, Nithin C. Babu, M. Naveen Sai Kiran, Aditi Prasad, Neeraj Badal, Rajiv Soundararajan
2025 A conf
WACV
Suhas Srinath, Aditya Chandrasekar, Hemang Jamadagni, Rajiv Soundararajan, Prathosh AP
2025 Misc conf
ICASSP
Shankhanil Mitra, Rajiv Soundararajan
2024 conf
SIGGRAPH (Conference Paper Track)
Nagabhushan Somraj, Kapil Choudhary, Sai Harsha Mupparaju, Rajiv Soundararajan
2024 J jnl
CoRR
Nagabhushan Somraj, Kapil Choudhary, Sai Harsha Mupparaju, Rajiv Soundararajan
2024 J jnl
CoRR
Diptanu De, Shankhanil Mitra, Rajiv Soundararajan
2024 conf
ICVGIP
Sanjot Sagar Totade, Nithin C. Babu, Shika Rao, Rajiv Soundararajan
2024 A* conf
AAAI
Shankhanil Mitra, Rajiv Soundararajan
2024 A conf
WACV
Suhas Srinath, Shankhanil Mitra, Shika Rao, Rajiv Soundararajan
2024 J jnl
IEEE Trans. Multim.
Shankhanil Mitra, Saiyam Jogani, Rajiv Soundararajan
2024 J jnl
CoRR
Nagabhushan Somraj, Adithyan Karanayil, Sai Harsha Mupparaju, Rajiv Soundararajan
2024 J jnl
CoRR
Suhas Srinath, Aditya Chandrasekar, Hemang Jamadagni, Rajiv Soundararajan, Prathosh A. P.
2023 J jnl
CoRR
Shankhanil Mitra, Rajiv Soundararajan
2023 J jnl
CoRR
Suhas Srinath, Shankhanil Mitra, Shika Rao, Rajiv Soundararajan
2023 A conf
WACV
Nithin C. Babu, Vignesh Kannan, Rajiv Soundararajan
2023 J jnl
IEEE Access
Vignesh Kannan, Sameer Malik, Nithin C. Babu, Rajiv Soundararajan
2023 A conf
WACV
Sameer Malik, Rajiv Soundararajan
2023 A* conf
SIGGRAPH Asia
Nagabhushan Somraj, Adithyan Karanayil, Rajiv Soundararajan
2023 J jnl
CoRR
Nagabhushan Somraj, Adithyan Karanayil, Rajiv Soundararajan
2023 A* conf
ICCV
Subhadeep Roy, Shankhanil Mitra, Soma Biswas, Rajiv Soundararajan
2023 J jnl
CoRR
Subhadeep Roy, Shankhanil Mitra, Soma Biswas, Rajiv Soundararajan
2023 conf
SIGGRAPH (Conference Paper Track)
Nagabhushan Somraj, Rajiv Soundararajan
2023 J jnl
CoRR
Nagabhushan Somraj, Rajiv Soundararajan
2022 A conf
BMVC
Shivam Chhirolya, Sameer Malik, Rajiv Soundararajan
2022 J jnl
CoRR
Shivam Chhirolya, Sameer Malik, Rajiv Soundararajan
2022 A* conf
ACM Multimedia
Shankhanil Mitra, Rajiv Soundararajan
2022 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Neeraj Badal, Rajiv Soundararajan, Ankur Garg, Abhishek Patil
2022 J jnl
CoRR
Vignesh Kannan, Sameer Malik, Rajiv Soundararajan
2022 A conf
WACV
Vijayalakshmi Kanchana, Nagabhushan Somraj, Suraj Yadwad, Rajiv Soundararajan
2022 A* conf
ISMAR
Nagabhushan Somraj, Pranali Sancheti, Rajiv Soundararajan
2022 J jnl
CoRR
Nagabhushan Somraj, Pranali Sancheti, Rajiv Soundararajan
2022 J jnl
Signal Process. Image Commun.
Nagabhushan Somraj, Manoj Surya Kashi, S. P. Arun, Rajiv Soundararajan
2021 J jnl
Signal Process. Image Commun.
Sameer Malik, Rajiv Soundararajan
2021 J jnl
IEEE Signal Process. Lett.
Shankhanil Mitra, Rajiv Soundararajan, Sumohana S. Channappayya
2021 J jnl
CoRR
Vijayalakshmi Kanchana, Nagabhushan Somraj, Suraj Yadwad, Rajiv Soundararajan
2020 B conf
ICIP
Sameer Malik, Rajiv Soundararajan
2020 J jnl
CoRR
Nagabhushan Somraj, Manoj Surya Kashi, S. P. Arun, Rajiv Soundararajan
2020 J jnl
Signal Process. Image Commun.
Biju Venkadath Somasundaran, Rajiv Soundararajan, Soma Biswas
2019 B conf
ICIP
Sameer Malik, Rajiv Soundararajan
2019 J jnl
Signal Process. Image Commun.
Rajiv Soundararajan, Soma Biswas
2019 A* conf
ISMAR
Suprith Balasubramanian, Rajiv Soundararajan
2019 J jnl
IEEE Trans. Image Process.
Pavan C. Madhusudana, Rajiv Soundararajan
2018 J jnl
Signal Process. Image Commun.
Praful Gupta, Anush Krishna Moorthy, Rajiv Soundararajan, Alan Conrad Bovik
2018 B conf
ICIP
Biju Venkadath Somasundaran, Rajiv Soundararajan, Soma Biswas
2017 J jnl
IEEE Signal Process. Lett.
Hisham Rahman, Rajiv Soundararajan, R. Venkatesh Babu
2017 J jnl
IEEE Signal Process. Lett.
Christos George Bampis, Praful Gupta, Rajiv Soundararajan, Alan C. Bovik
2016 J jnl
IEEE Trans. Inf. Theory
Onur Ozan Koyluoglu, Rajiv Soundararajan, Sriram Vishwanath
2013 conf
Human Vision and Electronic Imaging
Anish Mittal, Rajiv Soundararajan, Gautam S. Muralidhar, Alan C. Bovik, Joydeep Ghosh
2013 J jnl
IEEE Trans. Inf. Theory
Yeow-Khiang Chia, Rajiv Soundararajan, Tsachy Weissman
2013 J jnl
IEEE Signal Process. Lett.
Anish Mittal, Rajiv Soundararajan, Alan C. Bovik
2013 J jnl
Signal Image Video Process.
Rajiv Soundararajan, Alan C. Bovik
2013 J jnl
IEEE Trans. Circuits Syst. Video Technol.
Rajiv Soundararajan, Alan C. Bovik
2012 J jnl
J. Low Power Electron.
Rajiv Soundararajan, Ashok Srivastava
2012 J jnl
IEEE Trans. Inf. Theory
Rajiv Soundararajan, Sriram Vishwanath
2012 J jnl
CoRR
Yeow-Khiang Chia, Rajiv Soundararajan, Tsachy Weissman
2012 B conf
ISIT
Yeow-Khiang Chia, Rajiv Soundararajan, Tsachy Weissman
2012 J jnl
IEEE Trans. Image Process.
Rajiv Soundararajan, Alan C. Bovik
2012 J jnl
IEEE Trans. Inf. Theory
Rajiv Soundararajan, Aaron B. Wagner, Sriram Vishwanath
2012 conf
ISVLSI
Ashok Srivastava, Rajiv Soundararajan
2011 B conf
ISIT
Rajiv Soundararajan, Sriram Vishwanath
2011 Misc conf
ITA
Rajiv Soundararajan, Sriram Vishwanath
2011 Misc conf
ICASSP
Rajiv Soundararajan, Alan C. Bovik
2011 J jnl
CoRR
Onur Ozan Koyluoglu, Rajiv Soundararajan, Sriram Vishwanath
2011 conf
Allerton
Onur Ozan Koyluoglu, Rajiv Soundararajan, Sriram Vishwanath
2011 J jnl
Circuits Syst.
Rajiv Soundararajan, Ashok Srivastava, Siva Sankar Yellampalli
2010 conf
Human Vision and Electronic Imaging
Kalpana Seshadrinathan, Rajiv Soundararajan, Alan C. Bovik, Lawrence K. Cormack
2010 J jnl
CoRR
Rajiv Soundararajan, Aaron B. Wagner, Sriram Vishwanath
2010 J jnl
CoRR
Rajiv Soundararajan, Sriram Vishwanath
2010 J jnl
IEEE Trans. Image Process.
Kalpana Seshadrinathan, Rajiv Soundararajan, Alan Conrad Bovik, Lawrence K. Cormack
2010 B conf
ISIT
Rajiv Soundararajan, Aaron B. Wagner, Sriram Vishwanath
2010 J jnl
IEEE Trans. Circuits Syst. Video Technol.
Anush K. Moorthy, Kalpana Seshadrinathan, Rajiv Soundararajan, Alan Conrad Bovik
2009 B conf
DCC
Rajiv Soundararajan, Sriram Vishwanath
2009 J jnl
CoRR
Rajiv Soundararajan, Sriram Vishwanath
2009 B conf
ISIT
Rajiv Soundararajan, Sriram Vishwanath
2008 J jnl
CoRR
Rajiv Soundararajan, Sriram Vishwanath
2008 conf
ICC
Rajiv Soundararajan, Sriram Vishwanath
2008 J jnl
CoRR
Rajiv Soundararajan, Sriram Vishwanath
2008 ch.
Encyclopedia of Multimedia
Rajiv Soundararajan, Kalpana Seshadrinathan, Alan C. Bovik
tests/unit/test_database_exporters.py
← Index tests/unit/test_database_exporters.py python
"""
Unit tests for database exporters.
"""
import pytest
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from unittest.mock import Mock, patch, MagicMock

pytestmark = [pytest.mark.unit, pytest.mark.exporters]


# ============================================================================
# Sample Dataclass for Testing
# ============================================================================

@dataclass
class SampleData:
    """Sample dataclass for export testing."""
    sha256: str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    name: str = "test_sample"
    value: int = 42
    _id: str = "test_id"


# ============================================================================
# PrintExporter Tests
# ============================================================================

class TestPrintExporter:
    """Tests for PrintExporter class."""

    def test_initialization(self, mock_logger):
        """Test PrintExporter initialization."""
        from redb.extractors.database_exporters import PrintExporter

        exporter = PrintExporter(mock_logger, "test_index")

        assert exporter.index_prefix == "test_index"
        assert exporter.log is mock_logger

    def test_export_dataclass(self, mock_logger, capsys):
        """Test exporting a dataclass."""
        from redb.extractors.database_exporters import PrintExporter

        exporter = PrintExporter(mock_logger, "test_index")
        sample = SampleData()

        result = exporter.export(sample)

        assert result is True
        captured = capsys.readouterr()
        assert "test_sample" in captured.out

    def test_export_list_of_dataclasses(self, mock_logger, capsys):
        """Test exporting a list of dataclasses."""
        from redb.extractors.database_exporters import PrintExporter

        exporter = PrintExporter(mock_logger, "test_index")
        samples = [SampleData(name="sample1"), SampleData(name="sample2")]

        result = exporter.export(samples)

        assert result is True
        captured = capsys.readouterr()
        assert "sample1" in captured.out
        assert "sample2" in captured.out

    def test_export_dict(self, mock_logger, capsys):
        """Test exporting a dictionary."""
        from redb.extractors.database_exporters import PrintExporter

        exporter = PrintExporter(mock_logger, "test_index")
        data = {"key": "value", "number": 42}

        result = exporter.export(data)

        assert result is True
        captured = capsys.readouterr()
        assert "key" in captured.out
        assert "value" in captured.out

    def test_export_none(self, mock_logger):
        """Test exporting None."""
        from redb.extractors.database_exporters import PrintExporter

        exporter = PrintExporter(mock_logger, "test_index")

        result = exporter.export(None)

        assert result is True

    def test_export_empty_list(self, mock_logger):
        """Test exporting empty list."""
        from redb.extractors.database_exporters import PrintExporter

        exporter = PrintExporter(mock_logger, "test_index")

        result = exporter.export([])

        assert result is True


# ============================================================================
# ElasticsearchExporter Tests
# ============================================================================

class TestElasticsearchExporter:
    """Tests for ElasticsearchExporter class."""

    def test_initialization(self, mock_logger):
        """Test ElasticsearchExporter initialization."""
        with patch('redb.settings.get_elasticsearch_client') as mock_client:
            mock_client.return_value = Mock()
            from redb.extractors.database_exporters import ElasticsearchExporter

            exporter = ElasticsearchExporter(mock_logger, "test_index")

            assert exporter.index_prefix == "test_index"
            assert exporter.log is mock_logger
            assert exporter.client is not None

    def test_export_dataclass(self, mock_logger):
        """Test exporting a dataclass to Elasticsearch."""
        with patch('redb.settings.get_elasticsearch_client') as mock_get_client:
            mock_client = Mock()
            mock_get_client.return_value = mock_client

            from redb.extractors.database_exporters import ElasticsearchExporter

            exporter = ElasticsearchExporter(mock_logger, "test_index")
            sample = SampleData()

            result = exporter.export(
                sample,
                index="test_index",
                tag="test_tag",
                hashes={"sha256": sample.sha256}
            )

            assert result is True
            mock_client.index.assert_called()

    def test_export_list(self, mock_logger):
        """Test exporting a list to Elasticsearch."""
        with patch('redb.settings.get_elasticsearch_client') as mock_get_client:
            mock_client = Mock()
            mock_get_client.return_value = mock_client

            from redb.extractors.database_exporters import ElasticsearchExporter

            exporter = ElasticsearchExporter(mock_logger, "test_index")
            samples = [SampleData(name="sample1"), SampleData(name="sample2")]

            result = exporter.export(
                samples,
                index="test_index",
                tag="test_tag",
                hashes={"sha256": "test_hash"}
            )

            assert result is True
            # Should be called twice (once per sample)
            assert mock_client.index.call_count == 2

    def test_export_failure(self, mock_logger):
        """Test handling export failure."""
        with patch('redb.settings.get_elasticsearch_client') as mock_get_client:
            mock_client = Mock()
            mock_client.index.side_effect = Exception("Connection failed")
            mock_get_client.return_value = mock_client

            from redb.extractors.database_exporters import ElasticsearchExporter

            exporter = ElasticsearchExporter(mock_logger, "test_index")
            sample = SampleData()

            result = exporter.export(
                sample,
                index="test_index",
                tag="test_tag",
                hashes={"sha256": sample.sha256}
            )

            assert result is False
            mock_logger.error.assert_called()

    def test_export_with_custom_id(self, mock_logger):
        """Test exporting with custom _id field."""
        with patch('redb.settings.get_elasticsearch_client') as mock_get_client:
            mock_client = Mock()
            mock_get_client.return_value = mock_client

            from redb.extractors.database_exporters import ElasticsearchExporter

            exporter = ElasticsearchExporter(mock_logger, "test_index")
            sample = SampleData(_id="custom_id")

            result = exporter.export(
                sample,
                index="test_index",
                tag="test_tag",
                hashes={"sha256": sample.sha256}
            )

            assert result is True


# ============================================================================
# ClickHouseExporter Tests
# ============================================================================

class TestClickHouseExporter:
    """Tests for ClickHouseExporter class."""

    def test_initialization(self, mock_logger):
        """Test ClickHouseExporter initialization."""
        from redb.extractors.database_exporters import ClickHouseExporter

        mock_client = Mock()
        exporter = ClickHouseExporter(mock_logger, "test_index", client=mock_client)

        assert exporter.index_prefix == "test_index"
        assert exporter.log is mock_logger
        assert exporter.client is mock_client

    def test_export_tuple_data(self, mock_logger):
        """Test exporting tuple data to ClickHouse."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_client.insert.return_value = True
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            data = [["hash123", "sample.exe", 1024]]
            column_names = ["sha256", "name", "size"]
            column_type_names = ["String", "String", "UInt64"]

            result = exporter.export(
                (data, column_names, column_type_names),
                table="test_table"
            )

            assert result is True

    def test_export_multi_table(self, mock_logger):
        """Test exporting multi-table data to ClickHouse."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_client.insert.return_value = True
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            data = {
                'multi_table': True,
                'table1': {
                    'table': 'test_table1',
                    'data': [["hash123", "sample.exe"]],
                    'column_names': ["sha256", "name"],
                    'column_type_names': ["String", "String"]
                }
            }

            result = exporter.export(data)

            assert result is True

    def test_export_failure(self, mock_logger):
        """Test handling export failure."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_client.insert.side_effect = Exception("Connection failed")
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            data = [["hash123", "sample.exe", 1024]]
            column_names = ["sha256", "name", "size"]
            column_type_names = ["String", "String", "UInt64"]

            result = exporter.export(
                (data, column_names, column_type_names),
                table="test_table"
            )

            assert result is False

    def test_export_missing_table(self, mock_logger):
        """Test export without table name raises error."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            data = [["hash123", "sample.exe", 1024]]
            column_names = ["sha256", "name", "size"]
            column_type_names = ["String", "String", "UInt64"]

            result = exporter.export(
                (data, column_names, column_type_names)
                # Missing table parameter
            )

            assert result is False

    def test_get_client(self, mock_logger):
        """Test get_client method."""
        from redb.extractors.database_exporters import ClickHouseExporter

        mock_client = Mock()
        exporter = ClickHouseExporter(mock_logger, "test_index", client=mock_client)

        result = exporter.get_client()

        assert result is mock_client

    def test_get_client_creates_new(self, mock_logger):
        """Test get_client creates new client if none exists."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_new_client = Mock()
            mock_create.return_value = mock_new_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            result = exporter.get_client()

            assert result is mock_new_client


# ============================================================================
# DatabaseExporter Abstract Base Class Tests
# ============================================================================

class TestDatabaseExporter:
    """Tests for DatabaseExporter abstract base class."""

    def test_cannot_instantiate_abstract(self):
        """Test that DatabaseExporter cannot be instantiated directly."""
        from redb.extractors.database_exporters import DatabaseExporter

        with pytest.raises(TypeError):
            DatabaseExporter()

    def test_subclass_must_implement_export(self):
        """Test that subclasses must implement export method."""
        from redb.extractors.database_exporters import DatabaseExporter

        # Define incomplete subclass
        class IncompleteExporter(DatabaseExporter):
            pass

        with pytest.raises(TypeError):
            IncompleteExporter()


# ============================================================================
# Null/Edge Case Tests
# ============================================================================

class TestExporterEdgeCases:
    """Tests for edge cases in exporters."""

    def test_print_exporter_with_complex_nested_data(self, mock_logger, capsys):
        """Test PrintExporter with complex nested data."""
        from redb.extractors.database_exporters import PrintExporter

        exporter = PrintExporter(mock_logger, "test_index")
        data = {
            "level1": {
                "level2": {
                    "level3": [1, 2, 3]
                }
            },
            "array": [{"a": 1}, {"b": 2}]
        }

        result = exporter.export(data)

        assert result is True
        captured = capsys.readouterr()
        assert "level1" in captured.out

    def test_clickhouse_null_handling(self, mock_logger):
        """Test ClickHouse exporter handles null values."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_client.insert.return_value = True
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            # Multi-table data with null values
            data = {
                'multi_table': True,
                'table1': {
                    'table': 'test_table',
                    'data': [[None, "sample.exe", None]],
                    'column_names': ["sha256", "name", "size"],
                    'column_type_names': ["Nullable(String)", "String", "Nullable(UInt64)"]
                }
            }

            result = exporter.export(data)

            # Should handle nulls appropriately
            assert result is True

    def test_clickhouse_array_handling(self, mock_logger):
        """Test ClickHouse exporter handles array types."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_client.insert.return_value = True
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            data = {
                'multi_table': True,
                'table1': {
                    'table': 'test_table',
                    'data': [["hash", ["tag1", "tag2"]]],
                    'column_names': ["sha256", "tags"],
                    'column_type_names': ["String", "Array(String)"]
                }
            }

            result = exporter.export(data)

            assert result is True


# ============================================================================
# Type Conversion Tests
# ============================================================================

class TestExporterTypeConversions:
    """Tests for type conversions in exporters."""

    def test_clickhouse_datetime_handling(self, mock_logger):
        """Test ClickHouse exporter handles datetime values."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_client.insert.return_value = True
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            now = datetime.now(timezone.utc)
            data = {
                'multi_table': True,
                'table1': {
                    'table': 'test_table',
                    'data': [["hash", now]],
                    'column_names': ["sha256", "timestamp"],
                    'column_type_names': ["String", "DateTime64(3, 'UTC')"]
                }
            }

            result = exporter.export(data)

            assert result is True

    def test_print_exporter_datetime_serialization(self, mock_logger, capsys):
        """Test PrintExporter handles datetime serialization."""
        from redb.extractors.database_exporters import PrintExporter

        exporter = PrintExporter(mock_logger, "test_index")
        now = datetime.now(timezone.utc)
        data = {"timestamp": now, "name": "test"}

        result = exporter.export(data)

        assert result is True
        captured = capsys.readouterr()
        assert "timestamp" in captured.out


# ============================================================================
# Connection Context Manager Tests
# ============================================================================

class TestClickHouseConnectionManager:
    """Tests for ClickHouse connection context manager."""

    def test_clickhouse_connection_context(self, mock_logger):
        """Test clickhouse_connection context manager."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            with exporter.clickhouse_connection() as client:
                assert client is mock_client

            # Client should be closed after context exits
            mock_client.close.assert_called()

    def test_clickhouse_connection_close_on_error(self, mock_logger):
        """Test client is closed even if operation fails."""
        with patch('redb.settings.create_clickhouse_client') as mock_create:
            mock_client = Mock()
            mock_create.return_value = mock_client

            from redb.extractors.database_exporters import ClickHouseExporter

            exporter = ClickHouseExporter(mock_logger, "test_index")

            try:
                with exporter.clickhouse_connection() as client:
                    raise ValueError("Test error")
            except ValueError:
                pass

            # Client should still be closed
            mock_client.close.assert_called()