Repository navigation
Execute search_pattern's plan once and take matches from that run (#149) - #393
Conversation
search_pattern executed the table's plan twice: once over the columns its steps read, to find the positions of the matches, and again over every column, re-sorted and concatenated into one batch, to take the rows at those positions. That scanned the source and ran the upstream plan twice, held about twice the table at peak, and was not correct with tied sort keys: the second run may order tied rows differently, so a position could name a row the steps never matched. It now executes ordered_df() once, keeping every column. Each step predicate is evaluated batch by batch and only the boolean masks and the partition column are joined across batches; the matching rows are taken from the batches of that same execution with interleave, so the full rows are never concatenated. search_pattern_count keeps its own pruned path. A Rust test counts executions of a streaming source (sorted and unsorted, matches crossing batch and partition boundaries); Python tests check every column and the order of the result at every input layout, and that rows with tied sort keys are the rows that matched at every engine layout.
AI Code Review — Round 1Reviewed HEAD: FindingsNone. SpecThe implementation addresses issue #149: matching predicates and returned rows use the same full-column execution, eliminating positional correspondence between independent sorts. The execution-count test verifies one source execution. Regression tests cover multiple batches, tied sort keys, partition boundaries, all output columns and result order. Full-table concatenation is removed from the row-returning path. StandardsConsistent with AGENTS.md and CLAUDE.md: execution uses ordered_df(), remains in Rust under the existing detached execution boundary, and adds regression coverage and an Unreleased changelog entry. No public signature changes require stub updates. The worktree remains unchanged. AssessmentNo actionable correctness, security or maintainability defects found in the bound diff and related code. Batch-local positions are derived from the same collected batches used for predicate evaluation, and the unified matching loop preserves single-step and multi-step behavior. ObservationsThe supplied PR metadata matches the bound HEAD and base branch. All supplied CI checks were pending. The memory improvement is supported structurally by removing the full-row concatenation; peak RSS was not measured during this review. Memory still includes predicate masks, partition bookkeeping and matching-position vectors in addition to input and output rows. VerificationInspected the complete bound diff, related implementation, repository guidance, issue discussion, PR description and supplied checks. All 451 tests across test_search_pattern_count.py, test_search_pattern_rows.py, test_search_pattern_coercion.py and test_search_pattern_null_logic.py passed. cargo test --offline passed all 191 Rust tests, including the execution-count regression. cargo clippy --offline --all-targets -- -D warnings passed. git diff --check passed. An initial Python run exceeded its two-minute limit; its complete rerun passed. A broader Python regression run exceeded ten minutes without completing and is not counted as passing. No test processes remain running. SummaryNo findings requiring another fix round. The single-execution design resolves the duplicate work and tied-key row mismatch, with passing targeted Python and Rust coverage. Pending CI and unmeasured peak memory remain verification limitations. Needs another fix round: NO |
Why
search_patternexecuted the table's plan twice. The first run read the columns its steps use and found the positions of the matches. The second re-sorted the full table, concatenated every column into one batch, and took the rows at those positions. That had three costs:join,derive) ran twice.concat_batchesdeep-copied the whole table, so peak memory was about twice the table, even though matches are usually a tiny fraction of rows.(batch_idx, row_idx)from a projected first run.What changed
search_pattern_impl(src/ops/pattern_match.rs) now:table.ordered_df()once with every column. This is the table-order path AGENTS.md requires, so the old hand-built secondsortis gone.partition_bycolumn across batches.arrow::compute::interleave, column by column, so the full rows are never concatenated.Peak memory is the collected table, plus the masks and the partition column, plus the matching rows.
Not changed:
search_pattern_count. The issue comments say its separate fast path does not need to follow this refactor, so its pruned projected path is untouched. Because of that, the two functions no longer share steps 1 to 8, so I did not extract a common pipeline.The CHANGELOG has a
Fixedentry describing both the double execution and the tied-key row mix-up.How it was tested
search_pattern_executes_the_plan_once. ItsStreamingTablesource countsexecutecalls, and the test asserts the source is executed exactly once. It also checks the full result rows. It covers a shuffled source sorted byk, with and withoutpartition_by, and an in-order source split so that matches cross batch boundaries. The session batch size is 3, so the sorted output arrives in several batches. Against the old code the count would be 2, since it calledcollect()on two plans over the same source.py-ltseq/tests/test_search_pattern_rows.py:id,note), and the result order are compared with a Python reference at everydata_layout.engine_layout, with 10-way ties in the sort key and 7-row input batches, the returned rows are exactly the rows the steps matched, in key order, without duplicates.pytestover alltest_search_pattern*.pyfiles: 451 passed.pytestover every other test file that callssearch_pattern, plustest_no_materialization_rule.py: 1594 passed.cargo test: 191 passed.cargo clippy --all-targets -- -D warnings: clean.The full Python suite was not run locally; CI runs it on this PR.
Closes #149