fix: drop invalid expression rows#757
Conversation
Handle per-row expression render and cast failures by dropping only affected rows, preserving row identity in sync and async full-column paths, and failing when no valid rows remain. Fixes #749
PR #757 Review —
|
Greptile SummaryThis PR changes
|
| Filename | Overview |
|---|---|
| packages/data-designer-engine/src/data_designer/engine/column_generators/generators/expression.py | Adds per-row error handling to ExpressionColumnGenerator: catches EmptyTemplateRenderError, generic template errors, and cast errors individually, preserves original DataFrame index for retained rows, and raises UserTemplateError when all rows are dropped. |
| packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py | Adds expression-aware row-drop handling in _run_batch: tracks active_row_indices, routes expression results through _require_expression_row_drop_result, maps results back by row index, drops unmatched rows via _drop_row, and propagates UserTemplateError as a fatal error. |
| packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py | Sync builder updated to allow expression generators to shrink batches: adds _generator_supports_row_drops(), passes allow_resize=True for expression generators in both full-column and skip-aware merge paths, and adds _merge_row_dropped_generated_records() for index-based record reconciliation. |
| packages/data-designer-engine/tests/engine/column_generators/generators/test_expression.py | Adds four regression tests covering empty-render drops, template-error drops, cast-error drops, and the all-dropped fatal case; existing resource_provider mock updated to include run_config. |
| packages/data-designer-engine/tests/engine/dataset_builders/test_async_builder_integration.py | Adds two async integration tests: partial row-group shrink (verifies correct row-index tracking and buffer drop) and all-dropped fatal failure (verifies DatasetGenerationError propagation and empty output). |
| packages/data-designer-engine/tests/engine/dataset_builders/test_dataset_builder.py | Adds two sync builder tests: full-column batch shrink and skip-aware batch shrink, covering both code paths in dataset_builder.py. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant S as Scheduler / Builder
participant E as ExpressionColumnGenerator
participant B as BufferManager
S->>E: generate(batch_df with active_row_indices as index)
loop per row
E->>E: render_template(record)
alt EmptyTemplateRenderError or empty string
E->>E: "drop_counts[EMPTY] += 1"
else generic Exception
E->>E: "drop_counts[TEMPLATE_ERROR] += 1"
else cast fails
E->>E: "drop_counts[TYPE_CAST] += 1"
else success
E->>E: records.append + retained_indexes.append
end
end
alt all rows dropped
E-->>S: raise UserTemplateError (fatal)
S->>S: "_fatal_worker_error = exc → DatasetGenerationError"
else partial drops
E-->>S: "result_df (index = retained row indices)"
S->>S: _require_expression_row_drop_result(validate subset)
S->>S: _batch_result_by_row_index → dict[ri → record]
loop ri in rg_size
alt ri missing from result
S->>B: "_drop_row(rg, ri, exclude_columns={col})"
else ri present
S->>B: update_cell(rg, ri, col, value)
end
end
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant S as Scheduler / Builder
participant E as ExpressionColumnGenerator
participant B as BufferManager
S->>E: generate(batch_df with active_row_indices as index)
loop per row
E->>E: render_template(record)
alt EmptyTemplateRenderError or empty string
E->>E: "drop_counts[EMPTY] += 1"
else generic Exception
E->>E: "drop_counts[TEMPLATE_ERROR] += 1"
else cast fails
E->>E: "drop_counts[TYPE_CAST] += 1"
else success
E->>E: records.append + retained_indexes.append
end
end
alt all rows dropped
E-->>S: raise UserTemplateError (fatal)
S->>S: "_fatal_worker_error = exc → DatasetGenerationError"
else partial drops
E-->>S: "result_df (index = retained row indices)"
S->>S: _require_expression_row_drop_result(validate subset)
S->>S: _batch_result_by_row_index → dict[ri → record]
loop ri in rg_size
alt ri missing from result
S->>B: "_drop_row(rg, ri, exclude_columns={col})"
else ri present
S->>B: update_cell(rg, ri, col, value)
end
end
end
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py:2063-2070
**Third guard in `_require_expression_row_drop_result` is unreachable**
Given that the two preceding checks already pass — (1) no duplicate result indexes, and (2) every result index is a member of `active_index_set` — the result set is a subset of `active_row_indices` with no duplicates. Because `active_row_indices` is itself duplicate-free (built from `range(rg_size)` minus pre-dropped rows), `len(active_index_set) == len(active_row_indices)`, so `len(result_df) <= len(active_row_indices)` is guaranteed and the third `if` can never fire. This is dead code, not a runtime bug, but it may create a false sense of coverage for this guard.
Reviews (1): Last reviewed commit: "Merge branch 'main' into codex/issue-749..." | Re-trigger Greptile
| result_records = result_df.to_dict(orient="records") | ||
| if supports_row_drops: | ||
| return dict(zip(result_df.index.to_list(), result_records, strict=True)) | ||
| return dict(zip(active_row_indices, result_records, strict=True)) | ||
|
|
||
| def _get_rg_size(self, row_group: int) -> int: | ||
| try: | ||
| return self._row_groups.row_group_size(row_group) |
There was a problem hiding this comment.
Third guard in
_require_expression_row_drop_result is unreachable
Given that the two preceding checks already pass — (1) no duplicate result indexes, and (2) every result index is a member of active_index_set — the result set is a subset of active_row_indices with no duplicates. Because active_row_indices is itself duplicate-free (built from range(rg_size) minus pre-dropped rows), len(active_index_set) == len(active_row_indices), so len(result_df) <= len(active_row_indices) is guaranteed and the third if can never fire. This is dead code, not a runtime bug, but it may create a false sense of coverage for this guard.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py
Line: 2063-2070
Comment:
**Third guard in `_require_expression_row_drop_result` is unreachable**
Given that the two preceding checks already pass — (1) no duplicate result indexes, and (2) every result index is a member of `active_index_set` — the result set is a subset of `active_row_indices` with no duplicates. Because `active_row_indices` is itself duplicate-free (built from `range(rg_size)` minus pre-dropped rows), `len(active_index_set) == len(active_row_indices)`, so `len(result_df) <= len(active_row_indices)` is guaranteed and the third `if` can never fire. This is dead code, not a runtime bug, but it may create a false sense of coverage for this guard.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
📋 Summary
Expression columns currently fail the whole column when a single row renders to empty text or hits a row-specific Jinja/cast error. This PR makes those failures behave like other row-level generation failures: invalid rows are dropped with structured warning counts, while fully broken expressions still fail loudly.
🔗 Related Issue
Fixes #749
🔄 Changes
ExpressionColumnGeneratorso shrunken expression batches can be merged back safely.🔍 Attention Areas
async_scheduler.py— this is the core async merge/error-boundary change, including the distinction between partial row drops and all-dropped expression failures.dataset_builder.py— sync full-column and skip-aware paths now allow expression generators to shrink batches while preserving skip metadata.🧪 Testing
make testpasses (not run; focused engine checks were run instead)uv run --group dev pytest packages/data-designer-engine/tests/engine/column_generators/generators/test_expression.py packages/data-designer-engine/tests/engine/dataset_builders/test_dataset_builder.py::test_expression_column_row_drops_shrink_sync_batch packages/data-designer-engine/tests/engine/dataset_builders/test_dataset_builder.py::test_expression_column_row_drops_shrink_sync_skip_aware_batch packages/data-designer-engine/tests/engine/dataset_builders/test_async_builder_integration.py::test_expression_row_drops_shrink_async_row_group packages/data-designer-engine/tests/engine/dataset_builders/test_async_builder_integration.py::test_expression_all_dropped_async_row_group_fails_loudly -quv run --group dev pytest packages/data-designer-engine/tests/engine/dataset_builders/test_async_builder_integration.py -q.venv/bin/ruff check <changed files>.venv/bin/ruff format --check <changed files>✅ Checklist
Signed-off-bytrailers