From a3a2a139e70963d61ced85f99bca52c56741f817 Mon Sep 17 00:00:00 2001 From: Nick Pelikan Date: Wed, 3 Sep 2025 16:46:00 -0600 Subject: [PATCH 1/6] fix to df_to_html --- pkg-py/src/querychat/querychat.py | 15 +++- pkg-py/tests/test_query_function.py | 116 ++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 pkg-py/tests/test_query_function.py diff --git a/pkg-py/src/querychat/querychat.py b/pkg-py/src/querychat/querychat.py index 3f6ea616..5b142576 100644 --- a/pkg-py/src/querychat/querychat.py +++ b/pkg-py/src/querychat/querychat.py @@ -224,11 +224,20 @@ def df_to_html(df: IntoFrame, maxrows: int = 5) -> str: HTML string representation of the table """ + # Convert to Narwhals DataFrame if it's not already one if isinstance(df, (nw.LazyFrame, nw.DataFrame)): - df_short = df.lazy().head(maxrows).collect() - nrow_full = df.lazy().select(nw.len()).collect().item() + nw_df = df else: - raise TypeError("df must be a Narwhals DataFrame or LazyFrame") + # Try to convert using nw.from_native (supports pandas and other formats) + try: + nw_df = nw.from_native(df) + except Exception as e: + raise TypeError( + "df must be a Narwhals DataFrame, LazyFrame, or compatible DataFrame (e.g., pandas)", + ) from e + + df_short = nw_df.lazy().head(maxrows).collect() + nrow_full = nw_df.lazy().select(nw.len()).collect().item() # Generate HTML table table_html = df_short.to_pandas().to_html( diff --git a/pkg-py/tests/test_query_function.py b/pkg-py/tests/test_query_function.py new file mode 100644 index 00000000..982efacd --- /dev/null +++ b/pkg-py/tests/test_query_function.py @@ -0,0 +1,116 @@ +import sqlite3 +import tempfile +from pathlib import Path + +import pandas as pd +import pytest +from sqlalchemy import create_engine +from src.querychat.datasource import DataFrameSource, SQLAlchemySource +from src.querychat.querychat import df_to_html + + +@pytest.fixture +def sample_dataframe(): + """Create a sample pandas DataFrame for testing.""" + return pd.DataFrame( + { + "id": [1, 2, 3, 4, 5], + "name": ["Alice", "Bob", "Charlie", "Diana", "Eve"], + "age": [25, 30, 35, 28, 32], + "salary": [50000, 60000, 70000, 55000, 65000], + }, + ) + + +@pytest.fixture +def test_db_engine_with_data(): + """Create a temporary SQLite database with test data.""" + temp_db = tempfile.NamedTemporaryFile(delete=False, suffix=".db") # noqa: SIM115 + temp_db.close() + + conn = sqlite3.connect(temp_db.name) + cursor = conn.cursor() + + cursor.execute(""" + CREATE TABLE employees ( + id INTEGER PRIMARY KEY, + name TEXT, + age INTEGER, + salary REAL + ) + """) + + test_data = [ + (1, "Alice", 25, 50000), + (2, "Bob", 30, 60000), + (3, "Charlie", 35, 70000), + (4, "Diana", 28, 55000), + (5, "Eve", 32, 65000), + ] + + cursor.executemany( + "INSERT INTO employees (id, name, age, salary) VALUES (?, ?, ?, ?)", + test_data, + ) + + conn.commit() + conn.close() + + engine = create_engine(f"sqlite:///{temp_db.name}") + yield engine + + # Cleanup + Path(temp_db.name).unlink() + + +def test_df_to_html_with_dataframe_source_result(sample_dataframe): + """Test that df_to_html() works with results from DataFrameSource.execute_query().""" + source = DataFrameSource(sample_dataframe, "employees") + + # Execute query to get pandas DataFrame + result_df = source.execute_query("SELECT * FROM employees WHERE age > 25") + + # This should succeed after the fix + html_output = df_to_html(result_df) + + # Verify the HTML contains expected content + assert isinstance(html_output, str) + assert " 25") + + # This should succeed after the fix + html_output = df_to_html(result_df) + + # Verify the HTML contains expected content + assert isinstance(html_output, str) + assert " Date: Thu, 4 Sep 2025 09:18:51 -0400 Subject: [PATCH 2/6] refactor(df_to_html): Simplify --- pkg-py/src/querychat/querychat.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/pkg-py/src/querychat/querychat.py b/pkg-py/src/querychat/querychat.py index 5b142576..f449ede8 100644 --- a/pkg-py/src/querychat/querychat.py +++ b/pkg-py/src/querychat/querychat.py @@ -224,20 +224,15 @@ def df_to_html(df: IntoFrame, maxrows: int = 5) -> str: HTML string representation of the table """ - # Convert to Narwhals DataFrame if it's not already one - if isinstance(df, (nw.LazyFrame, nw.DataFrame)): - nw_df = df - else: - # Try to convert using nw.from_native (supports pandas and other formats) - try: - nw_df = nw.from_native(df) - except Exception as e: - raise TypeError( - "df must be a Narwhals DataFrame, LazyFrame, or compatible DataFrame (e.g., pandas)", - ) from e + ndf = nw.from_native(df) - df_short = nw_df.lazy().head(maxrows).collect() - nrow_full = nw_df.lazy().select(nw.len()).collect().item() + if isinstance(ndf, (nw.LazyFrame, nw.DataFrame)): + df_short = ndf.lazy().head(maxrows).collect() + nrow_full = ndf.lazy().select(nw.len()).collect().item() + else: + raise TypeError( + "Must be able to convert `df` into a Narwhals DataFrame or LazyFrame", + ) # Generate HTML table table_html = df_short.to_pandas().to_html( From 12100dd39d0912e7e23e53fbc0a7e825d31bf386 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 4 Sep 2025 09:21:45 -0400 Subject: [PATCH 3/6] chore: rename test file --- pkg-py/tests/{test_query_function.py => test_df_to_html.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg-py/tests/{test_query_function.py => test_df_to_html.py} (100%) diff --git a/pkg-py/tests/test_query_function.py b/pkg-py/tests/test_df_to_html.py similarity index 100% rename from pkg-py/tests/test_query_function.py rename to pkg-py/tests/test_df_to_html.py From 3489f9950f52544dc4698d82b82072a9a0eed10e Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 4 Sep 2025 09:26:59 -0400 Subject: [PATCH 4/6] refactor: rename test fixture --- pkg-py/tests/test_df_to_html.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg-py/tests/test_df_to_html.py b/pkg-py/tests/test_df_to_html.py index 982efacd..63d306c1 100644 --- a/pkg-py/tests/test_df_to_html.py +++ b/pkg-py/tests/test_df_to_html.py @@ -23,7 +23,7 @@ def sample_dataframe(): @pytest.fixture -def test_db_engine_with_data(): +def sample_sqlite(): """Create a temporary SQLite database with test data.""" temp_db = tempfile.NamedTemporaryFile(delete=False, suffix=".db") # noqa: SIM115 temp_db.close() @@ -82,9 +82,9 @@ def test_df_to_html_with_dataframe_source_result(sample_dataframe): assert "Eve" in html_output -def test_df_to_html_with_sqlalchemy_source_result(test_db_engine_with_data): +def test_df_to_html_with_sqlalchemy_source_result(sample_sqlite): """Test that df_to_html() works with results from SQLAlchemySource.execute_query().""" - source = SQLAlchemySource(test_db_engine_with_data, "employees") + source = SQLAlchemySource(sample_sqlite, "employees") # Execute query to get pandas DataFrame result_df = source.execute_query("SELECT * FROM employees WHERE age > 25") From 7d4183b5b9517aa4a2c761a0fb5de4a7ac27005b Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 4 Sep 2025 09:36:28 -0400 Subject: [PATCH 5/6] chore: add changelog item --- pkg-py/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg-py/CHANGELOG.md b/pkg-py/CHANGELOG.md index 36455431..c0d50cdd 100644 --- a/pkg-py/CHANGELOG.md +++ b/pkg-py/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [UNRELEASED] + +* Fixed an issue with the query tool when used with SQLAlchemy data sources. (@npelikan #79) + ## [0.2.0] - 2025-09-02 * `querychat.init()` now accepts a `client` argument, replacing the previous `create_chat_callback` argument. (#60) From 9a0eaa80e62c7b60eb3ab1169828afc3a83d8228 Mon Sep 17 00:00:00 2001 From: gadenbuie Date: Thu, 4 Sep 2025 13:43:29 +0000 Subject: [PATCH 6/6] `devtools::document()` (GitHub Actions) --- pkg-r/DESCRIPTION | 2 +- pkg-r/man/querychat_app.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg-r/DESCRIPTION b/pkg-r/DESCRIPTION index 9d87b99e..19dcfa16 100644 --- a/pkg-r/DESCRIPTION +++ b/pkg-r/DESCRIPTION @@ -42,4 +42,4 @@ Remotes: Config/testthat/edition: 3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 diff --git a/pkg-r/man/querychat_app.Rd b/pkg-r/man/querychat_app.Rd index 9ed93b87..156c0157 100644 --- a/pkg-r/man/querychat_app.Rd +++ b/pkg-r/man/querychat_app.Rd @@ -29,7 +29,7 @@ natural language queries. The app uses a pre-configured Shiny app built on way to chat with your data. } \examples{ -\dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +\dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf} # Pass in a data frame to start querychatting querychat_app(mtcars)