Skip to content

Commit 2a02dd8

Browse files
authored
Merge pull request #8 from Query-farm/extract-exception-handling-mixin
refactor: extract exception handling to Function base class
2 parents 4cc4ffc + bf63de0 commit 2a02dd8

5 files changed

Lines changed: 25 additions & 34 deletions

File tree

.beads/issues.jsonl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
{"id":"vgi-python-79e","title":"Unify ProtocolInput classes with shared base","description":"ProtocolInput classes in scalar_function.py:151-166 and table_in_out_function.py:109-142 have similar structure with batch and metadata fields. The table_in_out version adds is_finalize logic. Create shared base ProtocolInput in protocol_types.py with table_in_out extending it.","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-04T20:06:41.31917-05:00","created_by":"rusty","updated_at":"2026-01-04T20:07:16.240397-05:00"}
99
{"id":"vgi-python-a99","title":"Add settings accessor to function base classes","description":"Add a property to access DuckDB settings values in function implementations.\n\nChanges needed:\n- Add 'settings: dict[str, str]' property to Function base class\n- Property should return self.invocation.duckdb_settings or empty dict\n- Add convenience method like 'get_setting(name, default=None)'\n- Update ScalarFunction, TableFunctionGenerator, TableInOutFunction\n\nExample usage in function:\ndef compute(self, batch):\n tz = self.get_setting('timezone', 'UTC')\n # or\n tz = self.settings.get('timezone', 'UTC')","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-04T13:05:48.221602-05:00","created_by":"rusty","updated_at":"2026-01-04T13:20:41.171991-05:00","closed_at":"2026-01-04T13:20:41.171991-05:00","close_reason":"Implementation complete, all tests pass","dependencies":[{"issue_id":"vgi-python-a99","depends_on_id":"vgi-python-aad","type":"blocks","created_at":"2026-01-04T13:06:13.738212-05:00","created_by":"rusty"}]}
1010
{"id":"vgi-python-aad","title":"Design: DuckDB settings/pragmas access for VGI functions","description":"Design how VGI functions can declare required DuckDB settings/pragmas in their Meta class, and how these settings values should be passed during the bind phase.\n\nKey design decisions:\n1. How to declare required settings in function Meta (e.g., required_settings = ['timezone', 'threads'])\n2. How to add settings to Invocation dataclass\n3. How settings values should be accessed in function code\n4. Serialization format for settings in Arrow IPC\n\nRecommendation: Add 'duckdb_settings: dict[str, str] | None' to Invocation and 'required_settings: list[str]' to Meta class.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T13:05:47.619105-05:00","created_by":"rusty","updated_at":"2026-01-04T13:11:13.197139-05:00","closed_at":"2026-01-04T13:11:13.197139-05:00","close_reason":"Design document created at docs/design-duckdb-settings.md"}
11-
{"id":"vgi-python-bi8","title":"Extract common _process_with_exception_handling into mixin","description":"The _process_with_exception_handling and _process_and_validate methods are duplicated across scalar_function.py:296-346, table_function.py:386-438, and table_in_out_function.py:586-642. All follow same pattern: try _process_and_validate, catch exceptions, return OutputComplete with error message. Extract to ProcessingMixin that all function types inherit from.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-04T20:06:41.02111-05:00","created_by":"rusty","updated_at":"2026-01-04T20:07:15.947758-05:00","dependencies":[{"issue_id":"vgi-python-bi8","depends_on_id":"vgi-python-6o0","type":"blocks","created_at":"2026-01-04T20:07:49.181408-05:00","created_by":"rusty"}]}
11+
{"id":"vgi-python-bi8","title":"Extract common _process_with_exception_handling into mixin","description":"The _process_with_exception_handling and _process_and_validate methods are duplicated across scalar_function.py:296-346, table_function.py:386-438, and table_in_out_function.py:586-642. All follow same pattern: try _process_and_validate, catch exceptions, return OutputComplete with error message. Extract to ProcessingMixin that all function types inherit from.","status":"in_progress","priority":2,"issue_type":"task","created_at":"2026-01-04T20:06:41.02111-05:00","created_by":"rusty","updated_at":"2026-01-04T21:44:11.818421-05:00","dependencies":[{"issue_id":"vgi-python-bi8","depends_on_id":"vgi-python-6o0","type":"blocks","created_at":"2026-01-04T20:07:49.181408-05:00","created_by":"rusty"}]}
1212
{"id":"vgi-python-bku","title":"Change cardinality() method to property for consistency with output_schema","description":"Inconsistent access patterns: output_schema is a property but cardinality() is a method. Both return immutable data. Change cardinality() to a property for API consistency. Located in table_function.py:304-314.","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-04T20:06:53.211782-05:00","created_by":"rusty","updated_at":"2026-01-04T21:33:10.617152-05:00","closed_at":"2026-01-04T21:33:10.617152-05:00","close_reason":"PR #6 created: https://github.com/Query-farm/vgi-python/pull/6"}
1313
{"id":"vgi-python-bqb","title":"Update worker to handle DuckDB settings during bind","description":"Update vgi/worker.py to process DuckDB settings from Invocation during the bind phase.\n\nChanges needed:\n- Read settings from invocation.duckdb_settings\n- Validate that all required_settings (from Meta) are present in invocation\n- Pass settings to function instance for access\n- Log settings usage for debugging\n\nThe worker should validate settings early in bind to fail fast if required settings are missing.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-04T13:05:48.04037-05:00","created_by":"rusty","updated_at":"2026-01-04T13:20:41.17079-05:00","closed_at":"2026-01-04T13:20:41.17079-05:00","close_reason":"Implementation complete, all tests pass","dependencies":[{"issue_id":"vgi-python-bqb","depends_on_id":"vgi-python-aad","type":"blocks","created_at":"2026-01-04T13:06:13.714281-05:00","created_by":"rusty"}]}
1414
{"id":"vgi-python-c2b","title":"Add duckdb_settings field to Invocation class","description":"Update vgi/invocation.py to add a duckdb_settings field to the Invocation dataclass.\n\nChanges needed:\n- Add 'duckdb_settings: dict[str, str] | None = None' field to Invocation\n- Update serialize() to include settings in Arrow IPC batch\n- Update deserialize() to read settings from Arrow IPC batch\n- Handle None case (no settings requested)\n\nSerialization: Use a struct field with string key-value pairs or a map type.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-04T13:05:47.765077-05:00","created_by":"rusty","updated_at":"2026-01-04T13:20:41.167817-05:00","closed_at":"2026-01-04T13:20:41.167817-05:00","close_reason":"Implementation complete, all tests pass","dependencies":[{"issue_id":"vgi-python-c2b","depends_on_id":"vgi-python-aad","type":"blocks","created_at":"2026-01-04T13:06:13.664038-05:00","created_by":"rusty"}]}

vgi/function.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@
4242
import structlog
4343

4444
import vgi.ipc_utils
45+
import vgi.log
4546
from vgi.exceptions import ExecutionIdentifierError, SchemaValidationError
4647
from vgi.function_storage import FunctionStorage, FunctionStorageSqlite
4748
from vgi.invocation import InitResult, Invocation
4849
from vgi.metadata import MetadataMixin, ResolvedMetadata
50+
from vgi.output_complete import OutputComplete
4951

5052
__all__ = [
5153
"Function",
@@ -584,6 +586,22 @@ def _validate_output_schema(self, batch: pa.RecordBatch) -> None:
584586
context=f"output from {type(self).__name__}",
585587
)
586588

589+
@final
590+
def _should_terminate(self, result: OutputComplete) -> bool:
591+
"""Check if processing should terminate due to an exception."""
592+
return (
593+
result.log_message is not None
594+
and result.log_message.level == vgi.log.Level.EXCEPTION
595+
)
596+
597+
@final
598+
def _create_error_output(self, exception: Exception) -> OutputComplete:
599+
"""Create an OutputComplete with error message from exception."""
600+
return OutputComplete(
601+
batch=self.empty_output_batch,
602+
log_message=vgi.log.Message.from_exception(exception),
603+
)
604+
587605
@property
588606
def input_schema(self) -> pa.Schema:
589607
"""Return the input schema from the invocation.

vgi/scalar_function.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -309,18 +309,9 @@ def _process_with_exception_handling(
309309
try:
310310
return self._process_and_validate(generator, input_batch)
311311
except Exception as e:
312-
return OutputComplete(
313-
batch=self.empty_output_batch,
314-
log_message=vgi.log.Message.from_exception(e),
315-
)
312+
return self._create_error_output(e)
316313

317-
@final
318-
def _should_terminate(self, result: OutputComplete) -> bool:
319-
"""Check if processing should terminate due to an exception."""
320-
return (
321-
result.log_message is not None
322-
and result.log_message.level == vgi.log.Level.EXCEPTION
323-
)
314+
# _should_terminate inherited from Function
324315

325316
@abstractmethod
326317
def process(self, batch: pa.RecordBatch) -> ScalarOutputGenerator:

vgi/table_function.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -385,18 +385,9 @@ def _process_with_exception_handling(
385385
except StopIteration:
386386
raise
387387
except Exception as e:
388-
return OutputComplete(
389-
batch=self.empty_output_batch,
390-
log_message=vgi.log.Message.from_exception(e),
391-
)
388+
return self._create_error_output(e)
392389

393-
@final
394-
def _should_terminate(self, result: OutputComplete) -> bool:
395-
"""Check if processing should terminate due to an exception."""
396-
return (
397-
result.log_message is not None
398-
and result.log_message.level == vgi.log.Level.EXCEPTION
399-
)
390+
# _should_terminate inherited from Function
400391

401392
def process(self) -> OutputGenerator:
402393
"""Process batches during the DATA phase.

vgi/table_in_out_function.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -583,18 +583,9 @@ def _process_with_exception_handling(
583583
try:
584584
return self._process_and_validate(generator, batch)
585585
except Exception as e:
586-
return OutputComplete(
587-
batch=self.empty_output_batch,
588-
log_message=vgi.log.Message.from_exception(e),
589-
)
586+
return self._create_error_output(e)
590587

591-
@final
592-
def _should_terminate(self, result: OutputComplete) -> bool:
593-
"""Check if processing should terminate due to an exception."""
594-
return (
595-
result.log_message is not None
596-
and result.log_message.level == vgi.log.Level.EXCEPTION
597-
)
588+
# _should_terminate inherited from Function
598589

599590
def process(self, batch: pa.RecordBatch) -> OutputGenerator:
600591
"""Process input batches during the DATA phase.

0 commit comments

Comments
 (0)