Skip to content

1513: Feature/jupyter notebook support - #9

Open
martin-augment wants to merge 4 commits into
mainfrom
pr-1513-2026-03-16-21-05-25
Open

1513: Feature/jupyter notebook support#9
martin-augment wants to merge 4 commits into
mainfrom
pr-1513-2026-03-16-21-05-25

Conversation

@martin-augment

Copy link
Copy Markdown
Owner

1513: To review by AI

littleKitchen and others added 4 commits January 31, 2026 00:45
…s and examples

This PR implements all items from the checklist in issue apache#1398:

## Implementation Checklist

- [x] Add example .ipynb notebooks to python/examples/
  - getting_started.ipynb - Basic connection and queries
  - dataframe_api.ipynb - DataFrame transformations
  - distributed_queries.ipynb - Multi-stage query examples

- [x] Document notebook support in Python README
  - Added comprehensive Jupyter section with examples

- [x] Create ballista.jupyter module with magic commands
  - Full implementation with BallistaMagics class

- [x] Add %ballista connect/status/tables/schema line magics
  - connect: Connect to Ballista cluster
  - status: Show connection status
  - tables: List registered tables
  - schema: Show table schema
  - disconnect: Disconnect from cluster
  - history: Show query history

- [x] Add %%sql cell magic
  - Line magic for single-line queries
  - Cell magic for multi-line queries
  - Variable assignment support
  - --no-display and --limit options

- [x] Add explain_visual() method for query plan rendering
  - Generates DOT/SVG visualization
  - Supports Jupyter _repr_html_
  - Fallback when graphviz not installed

- [x] Add progress indicator support for long-running queries
  - collect_with_progress() method
  - Callback support for custom progress handling
  - Jupyter-aware display

- [x] Consider JupySQL integration
  - Documented as alternative in README

## Additional Features

- ExecutionPlanVisualization class for plan rendering
- tables() method on BallistaSessionContext
- Optional jupyter dependency in pyproject.toml
- Comprehensive test coverage (45 tests passing)

Closes apache#1398
@coderabbitai

coderabbitai Bot commented Mar 16, 2026

Copy link
Copy Markdown

Walkthrough

This pull request introduces comprehensive Jupyter notebook support to PyBallista. It adds three example notebooks demonstrating DataFrame API usage, distributed queries, and getting started with Ballista. The implementation includes a new IPython extension with magic commands for SQL execution and connection management, visualization of execution plans as SVG/HTML, progress tracking for long-running queries, table/schema exploration methods, and an optional jupyter dependency group. Supporting documentation is added to the Python README, gitignore entries for notebook checkpoints are updated, and test coverage is provided for all new functionality.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pr-1513-2026-03-16-21-05-25
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can customize the tone of the review comments and chat replies.

Configure the tone_instructions setting to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 7 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

else:
def decorator(func):
return func
return decorator

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing line_cell_magic stub breaks import without IPython

High Severity

The fallback stubs defined when IPython is not installed include line_magic, cell_magic, and magics_class, but do not include a stub for line_cell_magic. The sql method is decorated with @line_cell_magic, so importing ballista.jupyter without IPython installed will raise a NameError at class definition time, making the module completely unusable.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The decorator fallback is not implemented and trying to use the decorator when IPyhton is not available will lead to a runtime error. Prevents an early error when IPython is not available, making this notebook support partially unusable.

elif file_type == "csv":
self._ctx.register_csv(table_name, file_name)
else:
raise NotImplemented("Currently not supporting the inserted file format")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raise NotImplemented instead of raise NotImplementedError

Medium Severity

raise NotImplemented(...) is incorrect — NotImplemented is a special singleton constant used by rich comparison methods, not an exception class. This will raise a TypeError: exceptions must derive from BaseException instead of the intended error. The correct form is raise NotImplementedError(...).

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The name of the exception is wrong and any attempt to use it as an exception will fail because it is not extending from BaseException. Prevents hiding the real problem by failing with another error due to the wrong exception type.

if not tables:
return "No tables registered.\n\nUse ctx.register_parquet() or ctx.register_csv() to register tables."
schema_count = len(tables.keys())
table_count = len(tables.values())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

table_count incorrectly counts schemas instead of tables

Medium Severity

len(tables.values()) returns the number of schemas (dict entries), not the total number of tables. Since tables is a dict[str, list[str]], tables.values() is a view of lists, and len() gives the count of those lists (identical to schema_count). The total table count needs sum(len(v) for v in tables.values()).

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The values of the Dictionary are lists, so the length is the sum of their lengths, not just the number of values which would be the same as the length of the dictionary itself.

LIMIT 10
"""
for row in help_info.split("\n"):
print(row)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_show_help prints but never returns a string

Medium Severity

_show_help has return type Optional[str] but only prints to stdout and implicitly returns None. The ballista line magic (at "help" and empty-command branches) returns the result of _show_help(), which is always None. This also causes test failures since the tests call result.lower() on a None value.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The method return type is Optional but it does not return anything, i.e. implicitly it always returns None. This would break some unit tests because they try to use it as Some without checking

except Exception:
# Fallback if logical_plan() fails
plan_str = "Unable to retrieve execution plan"
return ExecutionPlanVisualization(plan_str, analyze=analyze)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain_visual ignores the analyze parameter for plan retrieval

Medium Severity

The analyze parameter is documented to include runtime statistics from actual execution, but the method always calls self.logical_plan().display_indent() regardless of the analyze value. The analyze flag is only passed through to ExecutionPlanVisualization where it's used solely for a display label string. When users call df.explain_visual(analyze=True), they get the same logical plan without any runtime statistics.

Fix in Cursor Fix in Web

if IPYTHON_AVAILABLE:
display(HTML(html))
else:
print("\n".join(status_lines))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_status returns None when connected, breaking callers

Low Severity

When connected, _status either calls display(HTML(...)) (IPython available) or print(...) (no IPython) but neither path returns a value. The method implicitly returns None, which causes the test test_status_when_connected to fail when it asserts string content in the result. The disconnected path correctly uses return.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The method return type is Optional but it does not return anything, i.e. implicitly it always returns None. This would break some unit tests because they try to use it as Some without checking


try:
# Query the table with LIMIT 0 to get schema without data
df = self.ctx.sql(f"SELECT * FROM {table_name} LIMIT 0")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQL injection risk in _schema via unescaped table name

Medium Severity

The _schema method constructs a SQL query by directly interpolating the user-provided table_name into an f-string: f"SELECT * FROM {table_name} LIMIT 0". Since table_name comes from user input via %ballista schema <table>, a malicious or accidental input could inject arbitrary SQL.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The table_name is an input provided by the user and used without any sanitation, so it could lead to an SQL injection. The value of table_name should be checked for any malicious characters which could break the SQL query.

@augmentcode

augmentcode Bot commented Mar 16, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR adds Jupyter notebook support to PyBallista via IPython magics and notebook-friendly helpers.

Changes:

  • Introduce ballista.jupyter extension with %ballista, %register, and %sql/%%sql magics for interactive workflows.
  • Add DistributedDataFrame.explain_visual() and collect_with_progress() plus a new ExecutionPlanVisualization wrapper for SVG/DOT rendering.
  • Export DistributedDataFrame and ExecutionPlanVisualization from ballista’s top-level package.
  • Add a jupyter optional dependency group (IPython) in pyproject.toml.
  • Update Python README with notebook usage examples and add three example notebooks under python/examples/.
  • Ignore Jupyter checkpoint directories in .gitignore.
Technical Notes: Plan visualization attempts to call Graphviz dot to produce SVG and falls back to HTML when unavailable.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 9 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

else:
raise NotImplemented("Currently not supporting the inserted file format")

@line_cell_magic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If IPython isn't available, the fallback stubs define line_magic/cell_magic but not line_cell_magic, so importing this module will raise NameError at the @line_cell_magic decorator.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The decorator fallback is not implemented and trying to use the decorator when IPyhton is not available will lead to a runtime error. Prevents an early error when IPython is not available, making this notebook support partially unusable.

elif file_type == "csv":
self._ctx.register_csv(table_name, file_name)
else:
raise NotImplemented("Currently not supporting the inserted file format")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raising NotImplemented(...) will itself raise TypeError because NotImplemented isn't an exception type, so unsupported formats won't fail cleanly.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The name of the exception is wrong and any attempt to use it as an exception will fail because it is not extending from BaseException. Prevents hiding the real problem by failing with another error due to the wrong exception type.

lines = [
{"content": f"Total: {table_count} table(s) in {schema_count} schema(s)", "is_info": True},
{"content": "Registered tables:", "is_info": True},
*[{"content": f"Schema: {schema_name}. Tables: {", ".join(list(table_names))}", "is_info": False}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This f-string has nested quotes ({", ".join(...)}) and is a syntax error, so ballista.jupyter won't import.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The string concatenation is broken here due to the mixed usage of quotes and double quotes. Prevents broken rendering of the notebook

return f"<p><strong>{name}:</strong> {value.strip()}</p>"

html = "".join(_format_html_status_output(line) for line in status_lines)
if IPYTHON_AVAILABLE:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several magic helpers (e.g., _status) print/display output but return None, while callers/tests treat the return value as a string; this will break the added unit tests and makes %ballista commands harder to use programmatically.

Severity: high

Other Locations
  • python/python/ballista/jupyter.py:272
  • python/python/ballista/jupyter.py:293
  • python/python/ballista/jupyter.py:346
  • python/python/ballista/jupyter.py:430
  • python/python/ballista/jupyter.py:477

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The method return type is Optional but it does not return anything, i.e. implicitly it always returns None. This would break some unit tests because they try to use it as Some without checking

if not tables:
return "No tables registered.\n\nUse ctx.register_parquet() or ctx.register_csv() to register tables."
schema_count = len(tables.keys())
table_count = len(tables.values())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

table_count = len(tables.values()) counts schemas (dict values) rather than total tables, so the reported "Total: X table(s)" will be incorrect when a schema contains multiple tables.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The values of the Dictionary are lists, so the length is the sum of their lengths, not just the number of values which would be the same as the length of the dictionary itself.

def _execute_sql(
self,
query: str,
display_results: bool = True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

display_results/limit are parsed from %%sql options but _execute_sql doesn't use them, so --no-display and --limit currently don't change notebook output.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The method does not use its arguments and this will confuse the notebook user, e.g. when the user tries to set a limit it will be ignored. The arguments should either be removed or used.

df = self._to_internal_df()
df.write_parquet(str(path), compression.value, compression_level)

def explain_visual(self, analyze: bool = False) -> "ExecutionPlanVisualization":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain_visual(analyze=True) sets the flag on ExecutionPlanVisualization, but plan_str is always built from logical_plan() and doesn't incorporate analyzed/runtime statistics, so the visualization won't actually differ when analyze=True.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

def __init__(self, address: str, config=None, runtime=None):
super().__init__(config, runtime)
self.address = address
self.session_id = self.session_id()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning self.session_id = self.session_id() shadows the inherited session_id() method, so any later call to session_id() would fail with a 'str' object is not callable-style error.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

self.address = address
self.session_id = self.session_id()

def get_tables(self) -> Optional[dict[str, str]]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_tables() is annotated as Optional[dict[str, str]], but it always returns a dict and the values are lists of table names; this mismatch can confuse callers and type checkers.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the PyBallista user experience by integrating robust Jupyter Notebook support. It empowers users with interactive SQL capabilities, rich data visualization, and detailed insights into query execution, making distributed data analysis more accessible and efficient within a notebook environment.

Highlights

  • Jupyter Notebook Integration: Introduced comprehensive support for Jupyter Notebooks, including SQL magic commands (%sql, %%sql), automatic HTML rendering of DataFrames, visual execution plan explanations, and progress indicators for long-running queries.
  • New Example Notebooks: Added three new example Jupyter notebooks: getting_started.ipynb, dataframe_api.ipynb, and distributed_queries.ipynb to demonstrate PyBallista's features.
  • Enhanced DataFrame and Session Context: Extended DistributedDataFrame with explain_visual() for SVG plan visualization and collect_with_progress() for interactive query progress. BallistaSessionContext now includes get_tables() for schema introspection.
  • Dependency Management: Updated pyproject.toml to include jupyter as an optional dependency and uv.lock to reflect new and updated dependencies required for Jupyter integration.
  • Gitignore Update: Added .ipynb_checkpoints to .gitignore to prevent Jupyter checkpoint files from being committed.
Changelog
  • .gitignore
    • Added .ipynb_checkpoints to ignore Jupyter checkpoint files.
  • python/README.md
    • Updated documentation to include a new section on Jupyter Notebook support, detailing HTML rendering, SQL magic commands, execution plan visualization, progress indicators, and new example notebooks.
  • python/examples/dataframe_api.ipynb
    • Added a new Jupyter notebook demonstrating the DataFrame API for Ballista.
  • python/examples/distributed_queries.ipynb
    • Added a new Jupyter notebook illustrating distributed query execution features in Ballista.
  • python/examples/getting_started.ipynb
    • Added a new Jupyter notebook providing a basic introduction to PyBallista, covering both Python API and SQL magic commands.
  • python/python/ballista/init.py
    • Exported DistributedDataFrame and ExecutionPlanVisualization classes for broader access.
  • python/python/ballista/extension.py
    • Implemented explain_visual and collect_with_progress methods for DistributedDataFrame, introduced ExecutionPlanVisualization class for visual plan rendering, and added get_tables to BallistaSessionContext.
  • python/python/ballista/jupyter.py
    • Added a new module for IPython magic commands, enabling direct interaction with Ballista clusters via %ballista and %sql commands in Jupyter.
  • python/python/tests/test_jupyter.py
    • Added new unit tests for the Ballista Jupyter magic commands.
  • python/python/tests/test_notebook_features.py
    • Added new unit tests for notebook-specific features like visual execution plans and progress tracking.
  • python/uv.lock
    • Updated Python version requirement to >=3.10 and synchronized various dependency versions, including adding IPython-related packages.
Activity
  • The pull request description explicitly states "To review by AI", indicating an initial automated review request.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces excellent support for Jupyter Notebooks, including magic commands, rich HTML display for DataFrames, execution plan visualization, and progress indicators. The implementation is well-structured and adds significant value for interactive use of PyBallista. I've included a few suggestions for improving robustness, fixing minor bugs in the magic commands, and enhancing error reporting.

if not tables:
return "No tables registered.\n\nUse ctx.register_parquet() or ctx.register_csv() to register tables."
schema_count = len(tables.keys())
table_count = len(tables.values())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The calculation for table_count is incorrect. len(tables.values()) will return the number of schemas, not the total number of tables. To get the total count of tables across all schemas, you should sum the lengths of the lists of table names.

Suggested change
table_count = len(tables.values())
table_count = sum(len(v) for v in tables.values())

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Gemini AI reviewer is correct! The values of the Dictionary are lists, so the length is the sum of their lengths, not just the number of values which would be the same as the length of the dictionary itself.

Comment on lines +327 to +328
except Exception:
pass # Ignore display errors

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Silencing all exceptions with except Exception: pass can hide potential bugs or issues with the environment (e.g., problems with IPython.display). It would be better to at least log the exception as a warning so that issues can be debugged if the progress indicator fails to display correctly.

Suggested change
except Exception:
pass # Ignore display errors
except Exception as e:
import warnings
warnings.warn(f"Failed to display progress in Jupyter: {e}")

Comment on lines +463 to +464
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The try...except block for running the dot command silences all errors. While the fallback to an HTML representation is good, it would be beneficial to log a warning when the dot command fails. This would help users diagnose why they are not seeing the SVG visualization (e.g., graphviz not installed or not in PATH).

Suggested change
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
pass
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired) as e:
import warnings
warnings.warn(f"Graphviz visualization failed: {e}. Falling back to text plan.")
pass

Comment on lines +536 to +537
def get_tables(self) -> Optional[dict[str, str]]:
"""Get tables and their respective schemas (in terms of database schema)."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type hint for the return value of get_tables is incorrect. The method returns a dictionary mapping schema names (strings) to lists of table names (lists of strings), so the type hint should be Dict[str, List[str]]. Currently, it's Optional[dict[str, str]]. Also, the docstring could be more precise about what is being returned.

Suggested change
def get_tables(self) -> Optional[dict[str, str]]:
"""Get tables and their respective schemas (in terms of database schema)."""
def get_tables(self) -> Dict[str, List[str]]:
"""Get a dictionary of schemas and their tables."""

Comment on lines +546 to +547
except (AttributeError, NotImplementedError):
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Silencing AttributeError and NotImplementedError can hide issues if the underlying catalog API changes. While returning an empty dictionary is a safe fallback, logging a warning would make it easier to detect and debug such problems in the future.

Suggested change
except (AttributeError, NotImplementedError):
pass
except (AttributeError, NotImplementedError) as e:
import warnings
warnings.warn(f"Could not retrieve tables from catalog: {e}")
pass

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Gemini AI reviewer is correct! The name of the exception is wrong and any attempt to use it as an exception will fail because it is not extending from BaseException. Prevents hiding the real problem by failing with another error due to the wrong exception type.

"Use: %ballista connect df://host:port"
)
else:
args = line.strip().split()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current argument parsing using line.strip().split() is not robust and will fail if file paths or table names contain spaces. Using shlex.split() provides more reliable parsing of command-line style arguments, correctly handling quoted strings. You will need to add import shlex at the top of the file.

Suggested change
args = line.strip().split()
args = shlex.split(line.strip())


def _format_html_tables_output(line: str, is_info: bool = False) -> str:
if is_info:
return f"<p><strong>{line}<strong></p>"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a typo in the closing HTML tag. <strong> should be closed with </strong>.

Suggested change
return f"<p><strong>{line}<strong></p>"
return f"<p><strong>{line}</strong></p>"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (6)
python/python/tests/test_notebook_features.py (2)

30-35: Consider reusing a session-scoped cluster fixture.

Starting a cluster in every test can make this suite unnecessarily slow and flaky. A scope="session" fixture with teardown would improve reliability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/tests/test_notebook_features.py` around lines 30 - 35, The
current ctx fixture calls setup_test_cluster() for every test which is
slow/flaky; create a session-scoped fixture (e.g., cluster or test_cluster) that
calls setup_test_cluster() once and yields the (address, port) pair with proper
teardown, then change the existing ctx fixture to consume that session-scoped
fixture and return BallistaSessionContext(address=f"df://{address}:{port}") so
tests reuse the single cluster instance; reference the setup_test_cluster
function and the ctx fixture/BallistaSessionContext to locate where to wire the
new session-scoped fixture.

206-217: These assertions are too weak to catch regressions.

isinstance(..., list) / is not None will pass even if table listing or HTML rendering is broken. Please assert expected content (e.g., registered table name present, HTML contains <table).

Also applies to: 228-233

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/tests/test_notebook_features.py` around lines 206 - 217, The
current assertions in test_tables_after_register_parquet and
test_tables_after_register_csv are too weak; update them to verify actual
content: after calling ctx.register_parquet("test_parquet",
...)/ctx.register_csv("test_csv", ...), call tables = ctx.tables() and assert
the registered table name appears in the returned listing (e.g., "test_parquet"
/ "test_csv" in tables) and, if tables can render HTML, assert that the HTML
representation contains "<table". Apply the same stronger assertions to the
other similar tests referenced (the tests around the second block with the same
pattern) using the ctx.tables(), ctx.register_parquet, ctx.register_csv and
test_* function names to locate and update the assertions.
python/python/tests/test_jupyter.py (1)

139-143: Test assertion is too weak.

The test only checks result is not None, but _tables() can return None when using IPython (it displays HTML instead). Consider either asserting the result contains expected content or testing in a non-IPython context explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/tests/test_jupyter.py` around lines 139 - 143, The test
test_tables_empty currently only asserts result is not None which is too weak
because connected_magics._tables() may return None in IPython (it renders HTML);
update the test to assert specific expected behavior: either (a) assert the
returned result contains an expected string/structure when _tables() returns a
value (e.g., check for "No tables" or an empty table marker in result), or (b)
force a non-IPython path by mocking/stubbing the IPython display/rendering so
_tables() returns its textual output and then assert the exact output; locate
the call to connected_magics._tables() in test_tables_empty and implement one of
these two fixes to make the assertion deterministic.
python/python/ballista/jupyter.py (2)

417-431: Inconsistent return behavior.

When history exists, the method prints and returns None. When empty, it returns a string. This inconsistency makes the return value unreliable for callers.

Suggested fix for consistency

Either always return a string:

     def _show_history(self) -> Optional[str]:
         """Show query history."""
         if not self._query_history:
             return "No queries executed yet."

         lines = ["Query History:", "-" * 60]
         for i, entry in enumerate(self._query_history[-10:], 1):  # Last 10 queries
             query_preview = entry["query"][:50] + "..." if len(entry["query"]) > 50 else entry["query"]
             query_preview = query_preview.replace("\n", " ")
             lines.append(f"{i}. [{entry['timestamp']}] ({entry['elapsed_seconds']:.2f}s)")
             lines.append(f"   {query_preview}")
         lines.append("-" * 60)

-        for row in lines:
-            print(row)
+        return "\n".join(lines)

Or always print:

     def _show_history(self) -> None:
         """Show query history."""
         if not self._query_history:
-            return "No queries executed yet."
+            print("No queries executed yet.")
+            return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/jupyter.py` around lines 417 - 431, _show_history
currently prints the history and returns None when history exists, but returns a
string when empty; make its return type consistent by always returning a string:
build the output by joining the lines (use the existing lines list), print the
joined string for backward compatibility, and then return the same joined
string; update usage of self._query_history and keep the method signature
(_show_history) as Optional[str] or stricter if desired.

238-243: Silently ignoring invalid --limit value may confuse users.

When --limit is followed by a non-integer, the error is silently ignored and the default limit is used. Consider providing feedback.

Suggested improvement
                 elif args[i] == "--limit" and i + 1 < len(args):
                     try:
                         limit = int(args[i + 1])
                         i += 1
                     except ValueError:
-                        pass
+                        print(f"Warning: Invalid --limit value '{args[i + 1]}', using default {limit}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/jupyter.py` around lines 238 - 243, The argument
parsing block that handles "--limit" (the snippet using args, i, and variable
limit in python/ballista/jupyter.py) currently swallows ValueError silently;
update the except ValueError branch to notify the user about the invalid value
instead of silently ignoring it (e.g., print a warning to stderr that includes
the invalid token args[i+1] and that the default limit will be used, or use
logging). Ensure you reference the same parsing logic (args, i, limit) and
import sys or logging if necessary, then preserve the existing behavior of
keeping the default limit while advancing i appropriately.
python/python/ballista/extension.py (1)

484-492: Consider handling file write errors.

The save method doesn't handle potential file operation errors (permissions, disk full, invalid path). Users calling save() would get an unhandled exception.

Proposed enhancement
     def save(self, path: str) -> None:
         """Save the visualization to a file (SVG or DOT format)."""
         if path.endswith(".dot"):
             content = self.to_dot()
         else:
             content = self.to_svg()

-        with open(path, "w") as f:
-            f.write(content)
+        with open(path, "w", encoding="utf-8") as f:
+            f.write(content)

At minimum, adding encoding="utf-8" ensures consistent behavior across platforms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/extension.py` around lines 484 - 492, The save method
lacks robust file error handling and explicit encoding; wrap the file write in a
try/except that catches OSError (and optionally Exception) around the
open/write, include encoding="utf-8" when opening the file, and re-raise a
clearer exception or raise a custom error with context (include the path and
original exception) so callers get a descriptive error instead of an unhandled
traceback; update the save function (which calls to_dot() or to_svg()) to
perform this guarded write and surface errors cleanly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/examples/getting_started.ipynb`:
- Around line 79-82: Replace the hard-coded scheduler host/port assignment with
the dynamic test-cluster setup: call setup_test_cluster() to obtain host and
port and use those values when constructing the BallistaSessionContext (the
current code assigns host, port = "localhost", "39431" and then calls
BallistaSessionContext(f"df://{host}:{port}")). Also update the other occurrence
that sets host/port manually (the later block around the other
BallistaSessionContext usage) to use setup_test_cluster() so the notebook uses a
reproducible, dynamic test cluster instead of a fixed port.

In `@python/python/ballista/extension.py`:
- Around line 536-548: The get_tables method has an incorrect return type: it is
annotated as Optional[dict[str, str]] but builds tables_info where each value is
a list of table names (list(catalog.schema(name=schema_name).table_names()));
update the signature of get_tables to Optional[dict[str, list[str]]] (or
dict[str, list[str]] if you never return None) and adjust any callers if
necessary to reflect that values are lists, keeping references to get_tables,
tables_info, and catalog.schema to locate the change.
- Around line 263-267: The parameter type for collect_with_progress currently
uses the built-in lowercase callable; change it to use typing.Callable by
importing Callable from typing and updating the signature of
collect_with_progress(callback: Optional[Callable] = None, ...). Locate the
function collect_with_progress in extension.py and add the Callable import
(alongside Optional if needed) to the module imports so the type hint is
correct.

In `@python/python/ballista/jupyter.py`:
- Around line 198-204: The code currently raises the singleton NotImplemented
(which is not an Exception) in the branch handling unsupported file types;
replace that with raising NotImplementedError (e.g., raise
NotImplementedError("Currently not supporting the inserted file format")) so
callers get the proper exception. Update the branch in the same block that calls
self._ctx.register_parquet and self._ctx.register_csv to raise
NotImplementedError with a clear message.
- Around line 330-331: schema_count is computed correctly as len(tables.keys())
but table_count is wrong because len(tables.values()) counts schemas, not
tables; change table_count to sum(len(v) for v in tables.values()) so it sums
the number of table names per schema (look for the variables schema_count,
table_count and the tables dict in the jupyter.py block to update this
computation).
- Around line 57-94: The stub fallback is missing line_cell_magic which causes a
NameError when IPython is absent; add a stub function named line_cell_magic that
mirrors the behavior of the existing line_magic and cell_magic stubs (accepts
either a callable or a name string and returns either the function or a
decorator wrapping the function), so code that uses `@line_cell_magic` or
`@line_cell_magic`("name") (and classes/functions like Magics) will work when
IPython is not installed.
- Around line 433-478: The _show_help method currently prints help_info
line-by-line and returns None, causing tests to fail; modify _show_help (the
function named _show_help and the local help_info variable) to return the full
help string (help_info) instead of (or in addition to) printing it so
callers/tests can inspect the text — e.g., remove the for loop printing or keep
printing but ensure the function ends with `return help_info` so a string is
returned.
- Around line 360-363: The SELECT uses an unvalidated table_name when calling
self.ctx.sql in the method (where df = self.ctx.sql(f"SELECT * FROM {table_name}
LIMIT 0") and schema = df.schema()), which allows SQL injection; validate
table_name before interpolating by enforcing a strict identifier pattern (e.g.
use a regex like ^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$ to allow
optional qualified names) and reject or raise ValueError for anything that fails
the check, or alternatively normalize/quote the identifier using the engine’s
safe identifier quoting if available, before constructing the query.

In `@python/python/tests/test_jupyter.py`:
- Around line 87-93: The test fails because _show_help() currently returns None;
update the _show_help function in jupyter.py so it returns the help text string
(instead of printing or doing nothing) so the test can call result.lower();
ensure the returned string contains the commands "connect", "status", "tables",
and "sql" so the assertions in test_show_help pass.
- Around line 127-131: The _status() method in jupyter.py currently
prints/displays the connected status (using display/HTML or print) but returns
None, causing the test test_status_when_connected to fail; update the _status()
implementation so that after building the status string (the same content passed
to display(HTML(...)) or print), it returns that string (ensure both the
connected and not-connected branches return the status string) so callers/tests
can assert on its contents; refer to the _status() method and the places where
it uses display/HTML and print to locate and change the behavior.

In `@python/README.md`:
- Around line 84-105: Add an explicit install step for notebook users: update
the Jupyter section to instruct users to install the optional Jupyter extras
before loading the extension (e.g., run pip install "ballista[jupyter]"), and
place this note immediately before the example that imports
BallistaSessionContext and before the %load_ext ballista.jupyter line so users
know to install the dependency first.

---

Nitpick comments:
In `@python/python/ballista/extension.py`:
- Around line 484-492: The save method lacks robust file error handling and
explicit encoding; wrap the file write in a try/except that catches OSError (and
optionally Exception) around the open/write, include encoding="utf-8" when
opening the file, and re-raise a clearer exception or raise a custom error with
context (include the path and original exception) so callers get a descriptive
error instead of an unhandled traceback; update the save function (which calls
to_dot() or to_svg()) to perform this guarded write and surface errors cleanly.

In `@python/python/ballista/jupyter.py`:
- Around line 417-431: _show_history currently prints the history and returns
None when history exists, but returns a string when empty; make its return type
consistent by always returning a string: build the output by joining the lines
(use the existing lines list), print the joined string for backward
compatibility, and then return the same joined string; update usage of
self._query_history and keep the method signature (_show_history) as
Optional[str] or stricter if desired.
- Around line 238-243: The argument parsing block that handles "--limit" (the
snippet using args, i, and variable limit in python/ballista/jupyter.py)
currently swallows ValueError silently; update the except ValueError branch to
notify the user about the invalid value instead of silently ignoring it (e.g.,
print a warning to stderr that includes the invalid token args[i+1] and that the
default limit will be used, or use logging). Ensure you reference the same
parsing logic (args, i, limit) and import sys or logging if necessary, then
preserve the existing behavior of keeping the default limit while advancing i
appropriately.

In `@python/python/tests/test_jupyter.py`:
- Around line 139-143: The test test_tables_empty currently only asserts result
is not None which is too weak because connected_magics._tables() may return None
in IPython (it renders HTML); update the test to assert specific expected
behavior: either (a) assert the returned result contains an expected
string/structure when _tables() returns a value (e.g., check for "No tables" or
an empty table marker in result), or (b) force a non-IPython path by
mocking/stubbing the IPython display/rendering so _tables() returns its textual
output and then assert the exact output; locate the call to
connected_magics._tables() in test_tables_empty and implement one of these two
fixes to make the assertion deterministic.

In `@python/python/tests/test_notebook_features.py`:
- Around line 30-35: The current ctx fixture calls setup_test_cluster() for
every test which is slow/flaky; create a session-scoped fixture (e.g., cluster
or test_cluster) that calls setup_test_cluster() once and yields the (address,
port) pair with proper teardown, then change the existing ctx fixture to consume
that session-scoped fixture and return
BallistaSessionContext(address=f"df://{address}:{port}") so tests reuse the
single cluster instance; reference the setup_test_cluster function and the ctx
fixture/BallistaSessionContext to locate where to wire the new session-scoped
fixture.
- Around line 206-217: The current assertions in
test_tables_after_register_parquet and test_tables_after_register_csv are too
weak; update them to verify actual content: after calling
ctx.register_parquet("test_parquet", ...)/ctx.register_csv("test_csv", ...),
call tables = ctx.tables() and assert the registered table name appears in the
returned listing (e.g., "test_parquet" / "test_csv" in tables) and, if tables
can render HTML, assert that the HTML representation contains "<table". Apply
the same stronger assertions to the other similar tests referenced (the tests
around the second block with the same pattern) using the ctx.tables(),
ctx.register_parquet, ctx.register_csv and test_* function names to locate and
update the assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a2cc295-ab35-492a-91e2-6ccacb6e7f24

📥 Commits

Reviewing files that changed from the base of the PR and between bc69192 and a188629.

⛔ Files ignored due to path filters (1)
  • python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .gitignore
  • python/README.md
  • python/examples/dataframe_api.ipynb
  • python/examples/distributed_queries.ipynb
  • python/examples/getting_started.ipynb
  • python/pyproject.toml
  • python/python/ballista/__init__.py
  • python/python/ballista/extension.py
  • python/python/ballista/jupyter.py
  • python/python/tests/test_jupyter.py
  • python/python/tests/test_notebook_features.py

Comment on lines +79 to +82
"# host, port = setup_test_cluster()\n",
"host, port = \"localhost\", \"39431\"\n",
"ctx = BallistaSessionContext(f\"df://{host}:{port}\")\n",
"\n",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid hard-coded scheduler port in a getting-started notebook.

This makes the happy path fail for most users unless they happen to run a cluster on 39431. Prefer the dynamic test-cluster setup for reproducibility.

Also applies to: 167-169

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/examples/getting_started.ipynb` around lines 79 - 82, Replace the
hard-coded scheduler host/port assignment with the dynamic test-cluster setup:
call setup_test_cluster() to obtain host and port and use those values when
constructing the BallistaSessionContext (the current code assigns host, port =
"localhost", "39431" and then calls
BallistaSessionContext(f"df://{host}:{port}")). Also update the other occurrence
that sets host/port manually (the later block around the other
BallistaSessionContext usage) to use setup_test_cluster() so the notebook uses a
reproducible, dynamic test cluster instead of a fixed port.

Comment on lines +263 to +267
def collect_with_progress(
self,
callback: Optional[callable] = None,
poll_interval: float = 0.5,
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect type hint: callable should be Callable.

The type hint uses lowercase callable which is the built-in function, not the typing annotation. Use Callable from the typing module instead.

Proposed fix

Add to imports:

 from typing import (
     Union,
     Optional,
     List,
+    Callable,
 )

Then update the parameter type:

     def collect_with_progress(
         self,
-        callback: Optional[callable] = None,
+        callback: Optional[Callable[[str, float], None]] = None,
         poll_interval: float = 0.5,
     ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/extension.py` around lines 263 - 267, The parameter
type for collect_with_progress currently uses the built-in lowercase callable;
change it to use typing.Callable by importing Callable from typing and updating
the signature of collect_with_progress(callback: Optional[Callable] = None,
...). Locate the function collect_with_progress in extension.py and add the
Callable import (alongside Optional if needed) to the module imports so the type
hint is correct.

Comment on lines +536 to +548
def get_tables(self) -> Optional[dict[str, str]]:
"""Get tables and their respective schemas (in terms of database schema)."""
try:
catalog = self.catalog()
schema_names = list(catalog.schema_names())
if schema_names is not None:
tables_info = {}
for schema_name in schema_names:
tables_info[schema_name] = list(catalog.schema(name=schema_name).table_names())
return tables_info
except (AttributeError, NotImplementedError):
pass
return {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect return type annotation.

The return type is annotated as Optional[dict[str, str]], but the method returns a dict where values are lists of table names (line 544 assigns list(catalog.schema(name=schema_name).table_names())).

Proposed fix
-    def get_tables(self) -> Optional[dict[str, str]]:
-        """Get tables and their respective schemas (in terms of database schema)."""
+    def get_tables(self) -> Optional[dict[str, list[str]]]:
+        """Get a mapping of schema names to their table names."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/extension.py` around lines 536 - 548, The get_tables
method has an incorrect return type: it is annotated as Optional[dict[str, str]]
but builds tables_info where each value is a list of table names
(list(catalog.schema(name=schema_name).table_names())); update the signature of
get_tables to Optional[dict[str, list[str]]] (or dict[str, list[str]] if you
never return None) and adjust any callers if necessary to reflect that values
are lists, keeping references to get_tables, tables_info, and catalog.schema to
locate the change.

Comment on lines +57 to +94
try:
from IPython.core.magic import Magics, magics_class, line_magic, cell_magic, line_cell_magic
from IPython.display import display, HTML

IPYTHON_AVAILABLE = True
except ImportError:
IPYTHON_AVAILABLE = False

# Provide stub classes for when IPython is not available
class Magics:
def __init__(self, shell=None):
self.shell = shell

def magics_class(cls):
return cls

def line_magic(name_or_func):
"""Stub line_magic decorator for when IPython is not available."""
# Handle both @line_magic and @line_magic("name") usage
if callable(name_or_func):
# Used as @line_magic without arguments
return name_or_func
else:
# Used as @line_magic("name") with arguments
def decorator(func):
return func
return decorator

def cell_magic(name_or_func):
"""Stub cell_magic decorator for when IPython is not available."""
# Handle both @cell_magic and @cell_magic("name") usage
if callable(name_or_func):
return name_or_func
else:
def decorator(func):
return func
return decorator

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing line_cell_magic stub will cause NameError.

The code imports line_cell_magic from IPython (line 58) and uses it as a decorator (line 205), but the fallback stubs only define line_magic and cell_magic. When IPython is unavailable, this will raise a NameError.

Proposed fix
     def cell_magic(name_or_func):
         """Stub cell_magic decorator for when IPython is not available."""
         # Handle both `@cell_magic` and `@cell_magic`("name") usage
         if callable(name_or_func):
             return name_or_func
         else:
             def decorator(func):
                 return func
             return decorator
+
+    def line_cell_magic(name_or_func):
+        """Stub line_cell_magic decorator for when IPython is not available."""
+        if callable(name_or_func):
+            return name_or_func
+        else:
+            def decorator(func):
+                return func
+            return decorator
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
from IPython.core.magic import Magics, magics_class, line_magic, cell_magic, line_cell_magic
from IPython.display import display, HTML
IPYTHON_AVAILABLE = True
except ImportError:
IPYTHON_AVAILABLE = False
# Provide stub classes for when IPython is not available
class Magics:
def __init__(self, shell=None):
self.shell = shell
def magics_class(cls):
return cls
def line_magic(name_or_func):
"""Stub line_magic decorator for when IPython is not available."""
# Handle both @line_magic and @line_magic("name") usage
if callable(name_or_func):
# Used as @line_magic without arguments
return name_or_func
else:
# Used as @line_magic("name") with arguments
def decorator(func):
return func
return decorator
def cell_magic(name_or_func):
"""Stub cell_magic decorator for when IPython is not available."""
# Handle both @cell_magic and @cell_magic("name") usage
if callable(name_or_func):
return name_or_func
else:
def decorator(func):
return func
return decorator
try:
from IPython.core.magic import Magics, magics_class, line_magic, cell_magic, line_cell_magic
from IPython.display import display, HTML
IPYTHON_AVAILABLE = True
except ImportError:
IPYTHON_AVAILABLE = False
# Provide stub classes for when IPython is not available
class Magics:
def __init__(self, shell=None):
self.shell = shell
def magics_class(cls):
return cls
def line_magic(name_or_func):
"""Stub line_magic decorator for when IPython is not available."""
# Handle both `@line_magic` and `@line_magic`("name") usage
if callable(name_or_func):
# Used as `@line_magic` without arguments
return name_or_func
else:
# Used as `@line_magic`("name") with arguments
def decorator(func):
return func
return decorator
def cell_magic(name_or_func):
"""Stub cell_magic decorator for when IPython is not available."""
# Handle both `@cell_magic` and `@cell_magic`("name") usage
if callable(name_or_func):
return name_or_func
else:
def decorator(func):
return func
return decorator
def line_cell_magic(name_or_func):
"""Stub line_cell_magic decorator for when IPython is not available."""
if callable(name_or_func):
return name_or_func
else:
def decorator(func):
return func
return decorator
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/jupyter.py` around lines 57 - 94, The stub fallback is
missing line_cell_magic which causes a NameError when IPython is absent; add a
stub function named line_cell_magic that mirrors the behavior of the existing
line_magic and cell_magic stubs (accepts either a callable or a name string and
returns either the function or a decorator wrapping the function), so code that
uses `@line_cell_magic` or `@line_cell_magic`("name") (and classes/functions like
Magics) will work when IPython is not installed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! The decorator fallback is not implemented and trying to use the decorator when IPyhton is not available will lead to a runtime error. Prevents an early error when IPython is not available, making this notebook support partially unusable.

Comment on lines +198 to +204
if file_type == "parquet":
self._ctx.register_parquet(table_name, file_name)
elif file_type == "csv":
self._ctx.register_csv(table_name, file_name)
else:
raise NotImplemented("Currently not supporting the inserted file format")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Raise NotImplementedError, not NotImplemented.

NotImplemented is a special singleton for rich comparison methods, not an exception. This line will raise a TypeError instead of the intended error.

Proposed fix
             if file_type == "parquet":
                 self._ctx.register_parquet(table_name, file_name)
             elif file_type == "csv":
                 self._ctx.register_csv(table_name, file_name)
             else:
-                raise NotImplemented("Currently not supporting the inserted file format")
+                raise NotImplementedError("Currently not supporting the inserted file format")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/jupyter.py` around lines 198 - 204, The code currently
raises the singleton NotImplemented (which is not an Exception) in the branch
handling unsupported file types; replace that with raising NotImplementedError
(e.g., raise NotImplementedError("Currently not supporting the inserted file
format")) so callers get the proper exception. Update the branch in the same
block that calls self._ctx.register_parquet and self._ctx.register_csv to raise
NotImplementedError with a clear message.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! The name of the exception is wrong and any attempt to use it as an exception will fail because it is not extending from BaseException. Prevents hiding the real problem by failing with another error due to the wrong exception type.

Comment on lines +360 to +363
try:
# Query the table with LIMIT 0 to get schema without data
df = self.ctx.sql(f"SELECT * FROM {table_name} LIMIT 0")
schema = df.schema()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

SQL injection risk with unvalidated table name.

The table_name is directly interpolated into the SQL query without validation. A malicious input like users; DROP TABLE users; -- could cause unintended behavior.

Suggested mitigation

Add basic validation to ensure the table name contains only valid identifier characters:

     def _schema(self, table_name: str) -> Optional[str]:
         """Show schema for a table."""
         if not table_name:
             return "Usage: %ballista schema <table_name>"

+        # Basic validation: allow only alphanumeric, underscore, dot (for schema.table)
+        import re
+        if not re.match(r'^[\w.]+$', table_name):
+            return f"Invalid table name: '{table_name}'"
+
         try:
             # Query the table with LIMIT 0 to get schema without data
             df = self.ctx.sql(f"SELECT * FROM {table_name} LIMIT 0")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/jupyter.py` around lines 360 - 363, The SELECT uses an
unvalidated table_name when calling self.ctx.sql in the method (where df =
self.ctx.sql(f"SELECT * FROM {table_name} LIMIT 0") and schema = df.schema()),
which allows SQL injection; validate table_name before interpolating by
enforcing a strict identifier pattern (e.g. use a regex like
^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$ to allow optional qualified
names) and reject or raise ValueError for anything that fails the check, or
alternatively normalize/quote the identifier using the engine’s safe identifier
quoting if available, before constructing the query.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! The table_name is an input provided by the user and used without any sanitation, so it could lead to an SQL injection. The value of table_name should be checked for any malicious characters which could break the SQL query.

Comment on lines +433 to +478
def _show_help(self) -> Optional[str]:
"""Show help for Ballista magic commands."""
help_info = """
Ballista Jupyter Magic Commands
================================

Connection:
%ballista connect <url> - Connect to Ballista cluster
%ballista disconnect - Disconnect from cluster
%ballista status - Show connection status

Exploration:
%ballista tables - List registered tables
%ballista schema <table> - Show table schema

Table-register:
%register [format] [schema.table_name] [file_path] - Register a new table in the current Ballista Context

Query:
%sql <query> - Execute single-line SQL query

%%sql [options] [var] - Execute multi-line SQL query
Options:
--no-display - Don't display results
--limit N - Limit displayed rows (default: 50)
var - Store result in variable

History:
%ballista history - Show recent query history

Examples:
%ballista connect df://localhost:50050
%ballista tables
%ballista schema orders

%sql SELECT COUNT(*) FROM orders

%%sql my_result
SELECT customer_id, SUM(amount) as total
FROM orders
GROUP BY customer_id
ORDER BY total DESC
LIMIT 10
"""
for row in help_info.split("\n"):
print(row)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Method prints but doesn't return, causing test failures.

The _show_help method prints the help text but returns None. However, tests (lines 89-99 in test_jupyter.py) expect it to return a string containing "connect", "status", etc. The tests will fail with AttributeError: 'NoneType' object has no attribute 'lower'.

Proposed fix
     def _show_help(self) -> Optional[str]:
         """Show help for Ballista magic commands."""
         help_info = """
 Ballista Jupyter Magic Commands
 ...
         """
-        for row in help_info.split("\n"):
-            print(row)
+        print(help_info)
+        return help_info
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/ballista/jupyter.py` around lines 433 - 478, The _show_help
method currently prints help_info line-by-line and returns None, causing tests
to fail; modify _show_help (the function named _show_help and the local
help_info variable) to return the full help string (help_info) instead of (or in
addition to) printing it so callers/tests can inspect the text — e.g., remove
the for loop printing or keep printing but ensure the function ends with `return
help_info` so a string is returned.

Comment on lines +87 to +93
def test_show_help(self, magics):
"""Test that help is shown correctly."""
result = magics._show_help()
assert "connect" in result.lower()
assert "status" in result.lower()
assert "tables" in result.lower()
assert "sql" in result.lower()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test will fail due to _show_help returning None.

As noted in the review of jupyter.py, _show_help() returns None but this test expects a string. The result.lower() call will raise AttributeError.

This will be fixed when _show_help() is updated to return the help string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/tests/test_jupyter.py` around lines 87 - 93, The test fails
because _show_help() currently returns None; update the _show_help function in
jupyter.py so it returns the help text string (instead of printing or doing
nothing) so the test can call result.lower(); ensure the returned string
contains the commands "connect", "status", "tables", and "sql" so the assertions
in test_show_help pass.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! The method return type is Optional but it does not return anything, i.e. implicitly it always returns None. This would break some unit tests because they try to use it as Some without checking

Comment on lines +127 to +131
def test_status_when_connected(self, connected_magics):
"""Test status shows connected state."""
result = connected_magics._status()
assert "Connected" in result
assert "Session ID" in result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test will fail: _status() returns None when connected.

Looking at _status() in jupyter.py (lines 298-321), when connected, the method either displays HTML or prints, but doesn't return a value. The test expects result to contain "Connected" and "Session ID", but result will be None.

This is related to the inconsistent return behavior in _status(). Either the method needs to return the status string, or the test needs to capture stdout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/python/tests/test_jupyter.py` around lines 127 - 131, The _status()
method in jupyter.py currently prints/displays the connected status (using
display/HTML or print) but returns None, causing the test
test_status_when_connected to fail; update the _status() implementation so that
after building the status string (the same content passed to display(HTML(...))
or print), it returns that string (ensure both the connected and not-connected
branches return the status string) so callers/tests can assert on its contents;
refer to the _status() method and the places where it uses display/HTML and
print to locate and change the behavior.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! The method return type is Optional but it does not return anything, i.e. implicitly it always returns None. This would break some unit tests because they try to use it as Some without checking

Comment thread python/README.md
Comment on lines +84 to +105
PyBallista provides first-class Jupyter notebook support with SQL magic commands and rich HTML rendering.

### HTML Table Rendering

DataFrames automatically render as styled HTML tables in Jupyter notebooks:

```python
from ballista import BallistaSessionContext

ctx = BallistaSessionContext("df://localhost:50050")
df = ctx.sql("SELECT * FROM my_table LIMIT 10")
df # Renders as HTML table via _repr_html_()
```

### SQL Magic Commands

For a more interactive SQL experience, load the Ballista Jupyter extension:

```python
# Load the extension
%load_ext ballista.jupyter

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit install step for notebook users.

The new section documents magics usage but doesn’t tell users to install the optional extra first, which can lead to immediate import/load failures.

Suggested doc patch
 ## Jupyter Notebook Support
 
 PyBallista provides first-class Jupyter notebook support with SQL magic commands and rich HTML rendering.
+
+Install Jupyter extras first:
+
+```bash
+pip install "ballista[jupyter]"
+```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/README.md` around lines 84 - 105, Add an explicit install step for
notebook users: update the Jupyter section to instruct users to install the
optional Jupyter extras before loading the extension (e.g., run pip install
"ballista[jupyter]"), and place this note immediately before the example that
imports BallistaSessionContext and before the %load_ext ballista.jupyter line so
users know to install the dependency first.

@claude

claude Bot commented Mar 16, 2026

Copy link
Copy Markdown

PR Review: Jupyter Notebook Support

Overall this is a useful feature addition with good documentation and a reasonable test suite. There are several bugs to address before merging.


Critical Bugs

1. NotImplemented vs NotImplementedError (jupyter.py:203)

NotImplemented is a built-in constant for binary operator fallbacks, not an exception. raise NotImplemented(...) produces TypeError: exceptions must derive from BaseException. Fix: use raise NotImplementedError(...).

2. Missing line_cell_magic stub (jupyter.py:57-93)

The except ImportError block stubs line_magic and cell_magic but omits line_cell_magic. Since @line_cell_magic decorates sql() at line 205, this causes a NameError whenever IPython is not installed.

3. F-string nested-quote SyntaxError (jupyter.py:336)

Reusing the same quote character inside an f-string expression is a SyntaxError on Python 3.10 and 3.11 (only allowed from 3.12+). Since pyproject.toml declares requires-python = ">=3.10", this breaks on the two oldest supported versions. Use a temporary variable or single-quoted separator.

4. table_count counts schemas, not tables (jupyter.py:330-331)

table_count = len(tables.values()) returns the number of schemas again (same as schema_count). Total table count should be: table_count = sum(len(v) for v in tables.values()).

5. session_id method shadowed by instance attribute (extension.py:534)

self.session_id = self.session_id() replaces the callable method with a string. Any later call to self.session_id() raises TypeError: 'str' object is not callable. Use a distinct attribute name such as self._session_id.


Medium Issues

6. _show_help() and _show_history() print instead of return (jupyter.py:417, 433)

Both methods print to stdout and return None. Tests at test_jupyter.py:88-99 assign the return value and assert on it, which fail with TypeError since result is None. Either return the strings or fix the tests to capture stdout via capsys.

7. Unclosed <strong> tag in _tables() (jupyter.py:342)

The closing tag reads <strong> instead of </strong>.

8. Wrong return type on get_tables() (extension.py:536)

Annotated as Optional[dict[str, str]] but the actual return type is dict[str, list[str]]. Optional is also unnecessary since an empty dict is returned on error.

9. analyze parameter is a no-op in explain_visual() (extension.py:234-261)

explain_visual(analyze=True) is documented to include runtime statistics but the implementation fetches the same logical plan string regardless. The flag only adds "(with statistics)" text in the HTML fallback. Either implement the analyzed path or remove the parameter and document the limitation.

10. SQL injection risk in _schema() (jupyter.py:362)

table_name from user input is interpolated directly into SQL without sanitization. Validate against a safe identifier pattern or quote/escape the identifier.


Minor Issues

  • extension.py:541: if schema_names is not None: is always True since list() never returns None. Change to if schema_names:.
  • jupyter.py:449: The %register help shows [schema.table_name] but the code does not parse a schema prefix. Update the help text or implement schema parsing.
  • jupyter.py:376-415: display_results and limit are accepted but never applied. The full result is always returned regardless of these values.

@martin-augment

Copy link
Copy Markdown
Owner Author

2. Missing line_cell_magic stub (jupyter.py:57-93)

The except ImportError block stubs line_magic and cell_magic but omits line_cell_magic. Since @line_cell_magic decorates sql() at line 205, this causes a NameError whenever IPython is not installed.

value:useful; category:bug; feedback: The Claude AI reviewer is correct! The decorator fallback is not implemented and trying to use the decorator when IPyhton is not available will lead to a runtime error. Prevents an early error when IPython is not available, making this notebook support partially unusable.

@martin-augment

Copy link
Copy Markdown
Owner Author

1. NotImplemented vs NotImplementedError (jupyter.py:203)

NotImplemented is a built-in constant for binary operator fallbacks, not an exception. raise NotImplemented(...) produces TypeError: exceptions must derive from BaseException. Fix: use raise NotImplementedError(...).

value:useful; category:bug; feedback: The Claude AI reviewer is correct! The name of the exception is wrong and any attempt to use it as an exception will fail because it is not extending from BaseException. Prevents hiding the real problem by failing with another error due to the wrong exception type.

@martin-augment

Copy link
Copy Markdown
Owner Author

4. table_count counts schemas, not tables (jupyter.py:330-331)

table_count = len(tables.values()) returns the number of schemas again (same as schema_count). Total table count should be: table_count = sum(len(v) for v in tables.values()).

value:useful; category:bug; feedback: The Claude AI reviewer is correct! The values of the Dictionary are lists, so the length is the sum of their lengths, not just the number of values which would be the same as the length of the dictionary itself.

@martin-augment

Copy link
Copy Markdown
Owner Author
  • jupyter.py:376-415: display_results and limit are accepted but never applied. The full result is always returned regardless of these values.

value:useful; category:bug; feedback: The Claude AI reviewer is correct! The method does not use its arguments and this will confuse the notebook user, e.g. when the user tries to set a limit it will be ignored. The arguments should either be removed or used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants