Neil D. B. Bruce

86 papers A* 11A 7B 12C 1Journal 35Unranked 19
YearRankTypeTitle / Venue / Authors
2025 B conf
ICTAI
Mina Khoshbazm Farimani, Neil D. B. Bruce, Shayan Sharif, Rozita A. Dara
2025 J jnl
CoRR
Marzieh Soltani, Rozita Dara, Zvonimir Poljak, Caroline Dubé, Neil D. B. Bruce, Shayan Sharif
2025 J jnl
Expert Syst. Appl.
Marzieh Soltani, Rozita Dara, Zvonimir Poljak, Caroline Dubé, Neil D. B. Bruce, Shayan Sharif
2025 J jnl
IEEE Trans. Pattern Anal. Mach. Intell.
Matthew Kowal, Mennatullah Siam, Md. Amirul Islam, Neil D. B. Bruce, Richard P. Wildes, Konstantinos G. Derpanis
2024 J jnl
Int. J. Comput. Vis.
Md. Amirul Islam, Matthew Kowal, Sen Jia, Konstantinos G. Derpanis, Neil D. B. Bruce
2023 J jnl
Int. J. Comput. Vis.
Md. Amirul Islam, Matthew Kowal, Konstantinos G. Derpanis, Neil D. B. Bruce
2022 A* conf
CVPR
Matthew Kowal, Mennatullah Siam, Md. Amirul Islam, Neil D. B. Bruce, Richard P. Wildes, Konstantinos G. Derpanis
2022 J jnl
CoRR
Matthew Kowal, Mennatullah Siam, Md. Amirul Islam, Neil D. B. Bruce, Richard P. Wildes, Konstantinos G. Derpanis
2022 A conf
BMVC
Md. Amirul Islam, Matthew Kowal, Patrick Esser, Björn Ommer, Konstantinos G. Derpanis, Neil D. B. Bruce
2022 J jnl
CoRR
Matthew Kowal, Mennatullah Siam, Md. Amirul Islam, Neil D. B. Bruce, Richard P. Wildes, Konstantinos G. Derpanis
2021 A* conf
ICCV
Md. Amirul Islam, Matthew Kowal, Sen Jia, Konstantinos G. Derpanis, Neil D. B. Bruce
2021 J jnl
CoRR
Md. Amirul Islam, Matthew Kowal, Sen Jia, Konstantinos G. Derpanis, Neil D. B. Bruce
2021 J jnl
CoRR
Md. Amirul Islam, Matthew Kowal, Sen Jia, Konstantinos G. Derpanis, Neil D. B. Bruce
2021 J jnl
IEEE Trans. Pattern Anal. Mach. Intell.
Mahmoud Kalash, Md. Amirul Islam, Neil D. B. Bruce
2021 J jnl
CoRR
Md. Amirul Islam, Matthew Kowal, Konstantinos G. Derpanis, Neil D. B. Bruce
2021 A* conf
ICLR
Md. Amirul Islam, Matthew Kowal, Patrick Esser, Sen Jia, Björn Ommer, Konstantinos G. Derpanis, Neil D. B. Bruce
2021 J jnl
CoRR
Md. Amirul Islam, Matthew Kowal, Patrick Esser, Sen Jia, Björn Ommer, Konstantinos G. Derpanis, Neil D. B. Bruce
2021 A conf
BMVC
Md. Amirul Islam, Matthew Kowal, Sen Jia, Konstantinos G. Derpanis, Neil D. B. Bruce
2021 J jnl
CoRR
Md. Amirul Islam, Matthew Kowal, Sen Jia, Konstantinos G. Derpanis, Neil D. B. Bruce
2020 J jnl
Int. J. Digit. Crime Forensics
Mahmoud Kalash, Mrigank Rochan, Noman Mohammed, Neil D. B. Bruce, Yang Wang, Farkhund Iqbal
2020 A conf
WACV
Rezaul Karim, Md. Amirul Islam, Neil D. B. Bruce
2020 J jnl
Image Vis. Comput.
Sen Jia, Neil D. B. Bruce
2020 A conf
BMVC
Md. Amirul Islam, Matthew Kowal, Konstantinos G. Derpanis, Neil D. B. Bruce
2020 J jnl
CoRR
Md. Amirul Islam, Matthew Kowal, Konstantinos G. Derpanis, Neil D. B. Bruce
2020 J jnl
CoRR
Md. Amirul Islam, Sen Jia, Neil D. B. Bruce
2020 A* conf
ICLR
Md. Amirul Islam, Sen Jia, Neil D. B. Bruce
2020 A* conf
CVPR
Sen Jia, Neil D. B. Bruce
2020 J jnl
CoRR
Sen Jia, Neil D. B. Bruce
2019 J jnl
CoRR
Rezaul Karim, Md. Amirul Islam, Neil D. B. Bruce
2019 B conf
CEC
Sasa Janjic, Parimala Thulasiraman, Neil D. B. Bruce
2019 C conf
SPIRE
Arezoo Abdollahi, Neil D. B. Bruce, Shahin Kamali, Rezaul Karim
2019 A conf
WACV
Rezaul Karim, Md. Amirul Islam, Neil D. B. Bruce
2019 J jnl
CoRR
Sen Jia, Neil D. B. Bruce
2018 B conf
ETRA
Karishma Singh, Mahmoud Kalash, Neil D. B. Bruce
2018 J jnl
CoRR
Md. Amirul Islam, Mrigank Rochan, Shujon Naha, Neil D. B. Bruce, Yang Wang
2018 conf
SSCI
Mohammad Asiful Hossain, Rezaul Karim, Ruppa K. Thulasiram, Neil D. B. Bruce, Yang Wang
2018 conf
NTMS
Mahmoud Kalash, Mrigank Rochan, Noman Mohammed, Neil D. B. Bruce, Yang Wang, Farkhund Iqbal
2018 conf
CRV
Rezaul Karim, Md. Amirul Islam, Noman Mohammed, Neil D. B. Bruce
2018 J jnl
CoRR
Rezaul Karim, Md. Amirul Islam, Neil D. B. Bruce
2018 B conf
IJCNN
Sasa Janjic, Parimala Thulasiraman, Neil D. B. Bruce
2018 J jnl
CoRR
Mahmoud Kalash, Md. Amirul Islam, Neil D. B. Bruce
2018 A* conf
CVPR
Md. Amirul Islam, Mahmoud Kalash, Neil D. B. Bruce
2018 J jnl
CoRR
Md. Amirul Islam, Mahmoud Kalash, Neil D. B. Bruce
2018 J jnl
CoRR
Calden Wloka, Toni Kunic, Iuliia Kotseruba, Ramin Fahimi, Nicholas Frosst, Neil D. B. Bruce, John K. Tsotsos
2018 A conf
BMVC
Md. Amirul Islam, Mahmoud Kalash, Neil D. B. Bruce
2018 J jnl
CoRR
Md. Amirul Islam, Mahmoud Kalash, Neil D. B. Bruce
2017 A* conf
CVPR
Md. Amirul Islam, Mrigank Rochan, Neil D. B. Bruce, Yang Wang
2017 J jnl
CoRR
Md. Amirul Islam, Shujon Naha, Mrigank Rochan, Neil D. B. Bruce, Yang Wang
2017 A* conf
HRI
Daniel J. Rea, Stela Hanbyeol Seo, Neil D. B. Bruce, James E. Young
2017 A conf
BMVC
Md. Amirul Islam, Mahmoud Kalash, Mrigank Rochan, Neil D. B. Bruce, Yang Wang
2017 B conf
RO-MAN
Daniel J. Rea, Mahdi Rahmani Hanzaki, Neil D. B. Bruce, James E. Young
2016 A* conf
CVPR
Neil D. B. Bruce, Christopher Catton, Sasa Janjic
2016 conf
CRV
Md. Amirul Islam, Neil D. B. Bruce, Yang Wang
2016 B conf
ETRA
Shafin Rahman, Neil D. B. Bruce
2016 conf
ETVIS
Mahmoud Kalash, Karishma Singh, M. Rasit Eskicioglu, Neil D. B. Bruce
2016 J jnl
Neurocomputing
Jonathan F. G. Boisvert, Neil D. B. Bruce
2016 ch.
Collaboration Meets Interactive Spaces
Barrett Ens, Eyal Ofek, Neil D. B. Bruce, Pourang Irani
2016 J jnl
Neurocomputing
Neil D. B. Bruce, Shafin Rahman, Diana Carrier
2016 J jnl
Image Vis. Comput.
Mrigank Rochan, Shafin Rahman, Neil D. B. Bruce, Yang Wang
2015 B conf
ICIP
Hamid Reza Nasrinpour, Neil D. B. Bruce
2015 conf
NIPS
Shafin Rahman, Neil D. B. Bruce
2015 conf
SUI
Barrett Ens, Eyal Ofek, Neil D. B. Bruce, Pourang Irani
2014 B conf
ICIP
Shafin Rahman, Mrigank Rochan, Yang Wang, Neil D. B. Bruce
2014 J jnl
Comput. Graph.
Neil D. B. Bruce
2014 B conf
HAI
Masayuki Nakane, James Everett Young, Neil D. B. Bruce
2014 conf
CRV
Mrigank Rochan, Shafin Rahman, Neil D. B. Bruce, Yang Wang
2014 B conf
ETRA
Neil D. B. Bruce
2013 B conf
Graphics Interface
Neil D. B. Bruce
2013 A* conf
UIST
Xing-Dong Yang, Khalad Hasan, Neil D. B. Bruce, Pourang Irani
2012 conf
ICIAR (1)
Xun Shi, Neil D. B. Bruce, John K. Tsotsos
2012 conf
CRV
Neil D. B. Bruce, Xun Shi, John K. Tsotsos
2011 conf
CVPR Workshops
Xun Shi, Neil D. B. Bruce, John K. Tsotsos
2011 J jnl
Frontiers Comput. Neurosci.
Neil D. B. Bruce, John K. Tsotsos
2011 conf
CRV
Neil D. B. Bruce, Xun Shi, Evgueni Simine, John K. Tsotsos
2009 A* conf
CVPR
Neil D. B. Bruce, Pierre Kornprobst
2009 B conf
ICIP
Neil D. B. Bruce, Pierre Kornprobst
2008 J jnl
Scholarpedia
John K. Tsotsos, Neil D. B. Bruce
2008 conf
WAPCV
Neil D. B. Bruce, John K. Tsotsos
2007 conf
WAPCV
Neil D. B. Bruce, John K. Tsotsos
2007 conf
ICIP (3)
Neil D. B. Bruce, Daniel P. Loach, John K. Tsotsos
2006 J jnl
Neurocomputing
Neil D. B. Bruce, John K. Tsotsos
2005 conf
CRV
Neil D. B. Bruce, John K. Tsotsos
2005 J jnl
Neurocomputing
Neil D. B. Bruce
2005 conf
NIPS
Neil D. B. Bruce, John K. Tsotsos
2004 conf
ICPR (1)
Neil D. B. Bruce
2003 conf
ICIP (1)
Neil D. B. Bruce, M. Ed Jernigan
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()