Ian Dunwell

52 papers A* 1B 5C 2Misc 3Journal 15Unranked 22
YearRankTypeTitle / Venue / Authors
2023 J jnl
Inf.
Sara de Freitas, Victoria S. Uren, Kristian Kiili, Manuel Ninaus, Panagiotis Petridis, Petros Lameras, Ian Dunwell, Sylvester Arnab, Stephen Jarvis, Kam Star
2021 J jnl
Smart Learn. Environ.
Petros Lameras, Sylvester Arnab, Sara de Freitas, Panagiotis Petridis, Ian Dunwell
2020 ch.
Visual Computing for Cultural Heritage
Stuart O'Connor, Simon Colreavy-Donnelly, Ian Dunwell
2019 J jnl
IEEE Trans. Games
Maurice Hendrix, Tyrone Bellamy-Wood, Sam McKay, Victoria Bloom, Ian Dunwell
2019 J jnl
BMC Medical Informatics Decis. Mak.
Elisa Puigdomènech, Anne Martin, Alexandra Rosewall Lang, Fulvio Adorni, Santiago Felipe Gomez, Brian McKinstry, Federica Prinelli, Laura A. Condon, Rajeeb Rashid, Maurizio Caon, Sarah Atkinson, Claudio L. Lafortuna, Valentina Ciociola, Janet Hanley, Lucy McCloughan, Castell Abat Conxa, Mireia Espallargues, Carme Carrion, Till Becker, Ian Dunwell, Kim C. M. Bul, Martina Camarenti, Chiara Crespi, Nithiya Jesuthasan, Gianfranco E. Modoni, Giovanna Rizzo, Marco Sacco, Aleksandra Sojic, Sarah A. Tabozzi, Olivier Grossenbacher, Mathieu Lemay, Enric Muntané Calvo, Felip Miralles, Silvia Orte, Marc Solà, Filip Velickovski, Mauro Brivio, Maria Renata Guarneri, Leonardo Angelini, Stefano Carrino, Elena Mugellini, Cesare Delaini, Dalia Morosini, Marco Decandia, Sara Facchinetti, Andrea Migliavacca, Silvana Mura, Luca Bianchi, Marco Mazzola, Sandro Repetti, Giuseppe Andreoni, Alessandra Mazzola, Paolo Perego, Carlo Emilio Standoli, Ciprian Candea, Gabriela Cândea, Massimiliano Azzolini, Luca Bianconi, Marco Costacurta, Cristiana Degano, Fabio Podda, Antonio Ascolese, Lucia Pannese, Yvonne Laird, George Scott, José C. E. Serrano, Sue Cobb, Neil S. Coulson, Alyson Langley
2019 B conf
CogSci
Stella Doukianou, Damon Daylamani-Zad, Petros Lameras, Ian Dunwell
2019 conf
IMCL
Ian Dunwell, Petros Lameras
2018 conf
ICL (1)
Ian Dunwell, Petros Lameras
2017 J jnl
Br. J. Educ. Technol.
Petros Lameras, Sylvester Arnab, Ian Dunwell, Craig D. Stewart, Samantha Clarke, Panagiotis Petridis
2017 ch.
Serious Games and Edutainment Applications
Ian Dunwell, Laura A. Condon, Kim C. M. Bul, Alexandra R. Lang, Sarah Atkinson, Neil S. Coulson, Emily Collins
2016 conf
SMAP
Ian Dunwell, Rory Dixon, Kim C. M. Bul, Maurice Hendrix, Pamela M. Kato, Antonio Ascolese
2015 J jnl
Br. J. Educ. Technol.
Sara de Freitas, David C. Gibson, Coert Du Plessis, Pat Halloran, Ed Williams, Matt Ambrose, Ian Dunwell, Sylvester Arnab
2015 J jnl
Int. J. Serious Games
Panagiotis Petridis, Kyriaki Hadjicosta, Victor Guang Shi, Ian Dunwell, Tim Baines, Ali Ziaee Bigdeli, Oscar F. Bustinza, Victoria S. Uren
2014 conf
VS-GAMES
Stella Doukianou, Panagiotis Petridis, Ian Dunwell, Joshua Cooper
2014 A* conf
CHI
Ian Dunwell, Sara de Freitas, Panagiotis Petridis, Maurice Hendrix, Sylvester Arnab, Petros Lameras, Craig D. Stewart
2014 J jnl
CoRR
Ian Dunwell, Panagiotis Petridis, Petros Lameras, Maurice Hendrix, Stella Doukianou, Mark Gaved
2014 J jnl
Int. J. Mob. Blended Learn.
Ann C. Jones, Mark Gaved, Agnes Kukulska-Hulme, Eileen Scanlon, Charlie Pearson, Petros Lameras, Ian Dunwell, Jan Jones
2014 Misc conf
CISIS
Ian Dunwell, Petros Lameras, Panagiotis Petridis, Sylvester Arnab, Sara de Freitas
2014 B conf
ICALT
Petros Lameras, Maggi Savin-Baden, Panagiotis Petridis, Ian Dunwell, Fotis Liarokapis
2014 conf
HCI (20)
Lucia Pannese, Dalia Morosini, Petros Lameras, Sylvester Arnab, Ian Dunwell, Till Becker
2014 J jnl
Int. J. Game Based Learn.
Ian Dunwell, Petros Lameras, Sara de Freitas, Panos Petridis, Maurice Hendrix, Sylvester Arnab, Kam Star
2014 conf
HCI (17)
Charn Pisithpunth, Panagiotis Petridis, Petros Lameras, Ian Dunwell
2013 C conf
Advances in Computer Entertainment
Lucas Paletta, Ian Dunwell, Mark Gaved, Jan Bobeth, Sofoklis Efremidis, Patrick Morris Luley, Agnes Kukulska-Hulme, Sara de Freitas, Petros Lameras, Stephanie Deutsch
2013 conf
VS-GAMES
Ian Dunwell, Petros Lameras, Kam Star, Sara de Freitas, Maurice Hendrix, Sylvester Arnab
2013 Misc conf
CISIS
Santi Caballé, Néstor Mora, Thanasis Daradoumis, David Gañán, Ian Dunwell, Anna Pierri
2013 J jnl
IEEE Pervasive Comput.
Björn W. Schuller, Ian Dunwell, Felix Weninger, Lucas Paletta
2013 J jnl
J. Electr. Comput. Eng.
Panagiotis Petridis, Ian Dunwell, Fotis Liarokapis, George Constantinou, Sylvester Arnab, Sara de Freitas, Maurice Hendrix
2013 conf
ICEBE
Maurice Hendrix, Petros Lameras, Panagiotis Petridis, Ian Dunwell, Sylvester Arnab, Craig D. Stewart, Jim Hensman, Sara de Freitas, Brian More
2013 J jnl
Comput. Educ.
Sylvester Arnab, Katherine Brown, Samantha Clarke, Ian Dunwell, Theodore Lim, Neil Suttie, Sandy Louchart, Maurice Hendrix, Sara de Freitas
2012 B conf
EC-TEL
Maurice Hendrix, Evgeny Knutov, Laurent Auneau, Aristidis Protopsaltis, Sylvester Arnab, Ian Dunwell, Panagiotis Petridis, Sara de Freitas
2012 J jnl
J. Univers. Comput. Sci.
Santi Caballé, David Gañán, Ian Dunwell, Anna Pierri, Thanasis Daradoumis
2012 C conf
ICEC
Ian Dunwell
2012 J jnl
Electron. Commer. Res.
Simon Scarle, Sylvester Arnab, Ian Dunwell, Panagiotis Petridis, Aristidis Protopsaltis, Sara de Freitas
2012 Misc conf
CISIS
Ian Dunwell, Panagiotis Petridis, Maurice Hendrix, Sylvester Arnab, Mohammad Al-Smadi, Christian Guetl
2012 conf
VS-GAMES
Samantha Clarke, Sylvester Arnab, Ian Dunwell, Katherine Brown
2012 conf
LiLe@WWW
Maurice Hendrix, Davide Taibi, Stefan Dietze, Aristos Protopsaltis, Ian Dunwell, Evangelia Mitsopoulou, Panagiotis D. Bamidis, Dimitris Spachos, Sara de Freitas, Panagiotis Petridis
2011 conf
INCoS
Ian Dunwell, Panagiotis Petridis, Sylvester Arnab, Aristidis Protopsaltis, Maurice Hendrix, Sara de Freitas
2011 conf
VS-GAMES
Panagiotis Petridis, Ian Dunwell, Sylvester Arnab, Simon Scarle, Adam Qureshi, Sara de Freitas, Aristidis Protopsaltis, Kam Star
2011 conf
INCoS
Santi Caballé, Néstor Mora, Ian Dunwell, David Gañán
2011 conf
VS-GAMES
Simon Scarle, Ian Dunwell, Thomas Bashford-Rogers, Elmedin Selmanovic, Kurt Debattista, Alan Chalmers, John A. Powell, Wendy Robertson
2011 ch.
Serious Games and Edutainment Applications
Sylvester Arnab, Panagiotis Petridis, Ian Dunwell, Sara de Freitas
2011 conf
SIGDOC
Aristidis Protopsaltis, Laurent Auneau, Ian Dunwell, Sara de Freitas, Panagiotis Petridis, Sylvester Arnab, Simon Scarle, Maurice Hendrix
2011 conf
WSKS
Santi Caballé, Ian Dunwell, Anna Pierri, Francesco Zurolo, David Gañán, Thanasis Daradoumis, Néstor Mora
2010 conf
VS-GAMES
Panagiotis Petridis, Ian Dunwell, Sara de Freitas, David Panzoli
2010 conf
SemWiki@ESWC
Ian Dunwell
2010 B conf
Intelligent Tutoring Systems (2)
David Panzoli, Adam Qureshi, Ian Dunwell, Panagiotis Petridis, Sara de Freitas, Genaro Rebolledo-Mendez
2010 conf
VS-GAMES
David Panzoli, Christopher Peters, Ian Dunwell, Stéphane Sanchez, Panagiotis Petridis, Aristidis Protopsaltis, Vincent Scesa, Sara de Freitas
2010 B conf
Intelligent Environments
Panagiotis Petridis, Oliver Goh, Ian Dunwell, David Wortley, Sara de Freitas
2009 conf
HCI (1)
Genaro Rebolledo-Mendez, Ian Dunwell, Erika Martínez-Mirón, María Dolores Vargas-Cerdán, Sara de Freitas, Fotis Liarokapis, Alma Rosa García-Gaona
2008 conf
IPT/EGVE
Ian Dunwell, John C. Whelan
2006
Ian Dunwell
2005 conf
TPCG
Ian Dunwell, John C. Whelan
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()