Skip to content

chore: sync upstream, and stop CLAUDE.md going stale - #54

Merged
anoop-narang merged 5 commits into
mainfrom
sync/upstream-2026-09-28
Sep 28, 2026
Merged

anoop-narang merged 5 commits into
mainfrom
sync/upstream-2026-09-28

Conversation

@anoop-narang

@anoop-narang anoop-narang commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Important

Merge this — do not squash it. A squash gives the result one parent, so upstream's history never enters main's ancestry. Simulated both onto main:

squash merge    merge-base stays 0033b15    upstream commits still missing: 1
regular merge   merge-base -> 5237fdd       upstream commits still missing: 0

Squashed, this PR silently undoes itself and nothing reports it. PR #51, the adopt merge that gave us upstream ancestry, was a 2-parent merge for the same reason.

Two fixes for problems this file caused. Another session followed CLAUDE.md, wrote correct commands, and they read as nonsense.

CLAUDE.md stated facts that expire

It pinned upstream at a commit id. True the hour it was written; wrong three days later when our own PR merged upstream, and worse on every upstream merge after that. A file whose job is telling agents what is true cannot hold facts that rot. It now states what stays true — main contains upstream's history in full — and gives the commands that answer where either side actually is.

Two more removed: the shuttle note counted the failures it expects, and the toolchain note deferred to a rust-toolchain.toml that does not exist in this repo. I wrote that from what such a repo usually has rather than from this one.

It got the remotes backwards

It said "the remote is origin in a fresh clone", which reads as origin is ours. In this working copy:

origin -> XiangpengHao/liquid-cache   (UPSTREAM)
fork   -> hotdata-dev/liquid-cache    (OURS)

A session wrote git merge origin/main meaning merge upstream, correct for this clone, and through the file it read as merging our own main into itself.

There is now a Remotes section saying to run git remote -v first, warning the names are not what you would guess, and every command names repositories by URL instead.

No Syncing section, which is what sent that session guessing

Added. Merge upstream into a branch and raise a PR; do not use GitHub's "Sync fork" button, which offers to discard our commits when the merge is not a fast-forward. It also describes the conflict our own upstreamed changes cause coming back — the content matches but the commit does not, so both sides look like they edited the same lines — and says to sync promptly, because alone that conflict is obvious and bundled with real upstream work it is not.

The sync itself

Merging 5237fdd (upstream's squash of our datafusion-contrib#521).

files changed vs our main            0
upstream commits we lack   1  ->     0
merge-base with upstream   0033b15 -> 5237fdd
workspace suite                      273 passed, 0 failed

Zero files change because upstream's only new commit is our own fix, which we already carry. The merge buys ancestry, not code — and retires the duplicate-change conflict now, while it is a one-line module list, rather than later bundled with real upstream work.

The conflict was exactly that: ours has mod batch_size_alignment;, upstream has nothing there. Kept ours. Verified afterwards that every declared module has a file and that the row_filter.rs bail did not come back with the merge.

…-contrib#521)

Some queries return rows that do not match their `WHERE` clause.

```sql
SELECT s FROM t WHERE NOT (s = s)
```

`s = s` is NULL wherever `s` is NULL, so `NOT (s = s)` is never true and
this should return nothing. On `main` it returns every row whose `s` is
NULL.

## Why

`build_row_filter` breaks a conjunction into separate predicates
(`split_conjunction`), then keeps only the conjuncts that reference a
column:

```rust
if required_indices_into_file_schema.is_empty() {
    return Ok(None);   // conjunct dropped
}
```

Simplification produces conjuncts that reference no column. `NOT (s =
s)` becomes `s IS NULL AND NULL` — a column conjunct, and a literal
`Boolean(NULL)` that reads nothing. The literal hit that branch and was
dropped.

Dropping it would be harmless if something else still applied it, but
nothing does: DataFusion removes the `FilterExec` once it believes the
predicate is fully pushed down, so the scan is the only place the
predicate is applied. A dropped conjunct makes the filter strictly
weaker than the one the user wrote, and rows that cannot match come
back.

## The fix

Keep the conjunct. A column-free candidate builds with an empty
projection mask, and the cached path evaluates it against a batch
carrying only the row count the selection implies — `RecordBatch` needs
that stated explicitly, since there is no array present to imply it.

## Reproducing

By hand: write a parquet file with a nullable `Utf8` column `s` where
some rows are NULL, register it through `LiquidCacheLocalBuilder`, and
run `SELECT s FROM t1 WHERE NOT (s = s)`. Expect no rows; on `main`
every NULL-`s` row comes back.

With the tests in this PR applied to `main` and the `row_filter.rs`
change reverted:

```
$ cargo test -p liquid-cache-datafusion-local constant_conjunct

test constant_null_conjunct_is_still_applied ... FAILED
test three_way_partition_reconstructs_the_scan ... FAILED

---- constant_null_conjunct_is_still_applied ----
  left: ["<null>", "<null>", ...]     # 4000 rows returned
 right: []                            # none expected

---- three_way_partition_reconstructs_the_scan ----
  left: 14664     # rows across the three partitions
 right: 12000     # rows in the table

test result: FAILED. 0 passed; 2 failed
```

## The tests

`src/datafusion-local/src/tests/constant_conjunct.rs`, both cold and
warm cache:

- **`constant_null_conjunct_is_still_applied`** — the direct case above.
The fixture is 12000 rows with every third `s` NULL, giving the 4000.
- **`three_way_partition_reconstructs_the_scan`** — the general
property, and the more useful regression guard: for any predicate `P`,
`WHERE P`, `WHERE NOT P` and `WHERE P IS NULL` must together return each
row exactly once. Here rows whose `P` was NULL came back from both
`WHERE NOT P` and `WHERE P IS NULL`.

Found by a randomized query fuzzer using that three-way partition as its
oracle.
…ache into sync/upstream-2026-09-28

# Conflicts:
#	src/datafusion-local/src/tests/mod.rs
The file pinned upstream at a commit id. That was true the hour it was
written and wrong three days later when our own PR merged upstream, and
it would have gone on rotting on every upstream merge. A file whose job
is telling agents what is true cannot hold facts that expire. It now says
`main` contains upstream's history in full, which stays true, and gives
the commands that answer where either side actually is.

It also said "the remote is `origin` in a fresh clone", which reads as
"origin is ours". In this working copy `origin` is *upstream* and the
fork is a second remote named `fork` — the reverse. A session following
this file wrote `git merge origin/main` meaning merge upstream, correctly
for this clone, and it read as merging our own main into itself. There is
now a Remotes section saying to run `git remote -v` first and warning
that the names are not what you would guess, and every command names
repositories by URL.

Adds a Syncing section, because the omission is what sent that session
looking. It says to merge upstream into a branch and raise a PR rather
than using GitHub's "Sync fork" button, which offers to discard our
commits when the merge is not a fast-forward, and it describes the
conflict our own upstreamed changes cause on the way back: the content
matches but the commit does not, so both sides appear to have edited the
same lines. Resolve by keeping ours, and sync promptly, because alone
that conflict is obvious and bundled with real upstream work it is not.

Two more expiring claims removed. The shuttle note counted the failures
it expects; it now says which tests fail and why. The toolchain note
deferred to a `rust-toolchain.toml` that does not exist in this repo --
written from what such a repo usually has rather than from this one --
and now says there is none, which is the reason the pin is needed.
@anoop-narang
anoop-narang requested a review from a team as a code owner September 28, 2026 04:24
@anoop-narang
anoop-narang requested review from rohan-hotdata and removed request for a team September 28, 2026 04:24
Comment thread CLAUDE.md Outdated
Comment thread CLAUDE.md Outdated
claude[bot]
claude Bot previously approved these changes Sep 28, 2026
@codecov

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…urs"

Review found the Syncing section walking into the trap the Branches
section warns about. Its only fetch named upstream, so local `main` was
never refreshed, and the sync branch was cut from it — a stale one makes
the sync PR revert fork commits merged since, with nothing to say so. It
now fetches this fork first and branches from what came back, and uses
each `FETCH_HEAD` immediately, since it holds only the last fetch. My own
sync branch escaped this only because I had refreshed local `main` an
hour earlier for unrelated reasons.

"Resolve by keeping ours" was too broad. It is right for the returned
squash of our own change, but a conflict hunk can hold an unrelated
upstream edit on the same lines — a module appended next to the returned
one — and taking the whole hunk from our side drops it silently. Now:
keep ours for the returned change, keep any other upstream edit in the
same hunk, and read the hunk rather than resolving by rule.

Also records that a sync PR must be merged and not squashed. A squash
gives the result one parent, so upstream's history never enters `main`'s
ancestry: the merge-base does not move and the same upstream commits stay
missing, with no error. Simulated both onto `main` — squashed, the
merge-base stayed at the old upstream commit and the sync was undone;
merged, it advanced to upstream's tip. The section now says to verify
`git merge-base main FETCH_HEAD` afterwards.
claude[bot]
claude Bot previously approved these changes Sep 28, 2026
Comment thread CLAUDE.md Outdated
Review found the verify line checking neither side of what it claimed.
`git merge-base main FETCH_HEAD` reads local `main`, which does not
contain a merge made on GitHub, and `FETCH_HEAD` is whatever was fetched
last — fetch the fork to refresh `main` and it compares the fork against
itself. Run as written it returns our own tip and looks like a pass.

It now captures upstream's tip before fetching the fork over it, and
tests the property directly. Run against the current state, with this PR
unmerged, it correctly reports not-ok; the old line reported our own
main and told the reader nothing.

Third time in this file that `FETCH_HEAD` holding only the last fetch has
produced a wrong instruction. The sync recipe above now says so where the
fetches are, rather than leaving each site to remember it.
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

📊 Benchmark Comparison

Current: 4ebacd16 (Liquid) vs Baseline: 4ebacd16 (DataFusionDefault)

Query Cold Time Δ Warm Time Δ CPU Time Δ
Q1 3.0ms (2.0ms) +50.0% 0.000ms (0.000ms) +0.0% 0.000ms (0.000ms) +0.0%
Q2 14.0ms (5.0ms) +180.0% 6.0ms (4.5ms) +33.3% 10.5ms (6.0ms) +75.0%
Q3 20.0ms (12.0ms) +66.7% 7.0ms (12.5ms) -44.0% 3.0ms (22.5ms) -86.7%
Q4 15.0ms (12.0ms) +25.0% 4.5ms (11.0ms) -59.1% 5.0ms (24.0ms) -79.2%
Q5 69.0ms (49.0ms) +40.8% 53.0ms (51.0ms) +3.9% 4.0ms (25.0ms) -84.0%
Q6 219.0ms (106.0ms) +106.6% 89.0ms (103.5ms) -14.0% 26.5ms (79.0ms) -66.5%
Q7 1.0ms (1.0ms) +0.0% 1.0ms (0.000ms) +inf% 0.000ms (0.000ms) +0.0%
Q8 9.0ms (5.0ms) +80.0% 7.5ms (5.5ms) +36.4% 11.5ms (6.0ms) +91.7%
Q9 118.0ms (86.0ms) +37.2% 88.0ms (83.0ms) +6.0% 5.0ms (43.0ms) -88.4%
Q10 112.0ms (93.0ms) +20.4% 83.5ms (90.5ms) -7.7% 8.5ms (61.5ms) -86.2%
Q11 45.0ms (25.0ms) +80.0% 24.0ms (26.0ms) -7.7% 45.5ms (35.0ms) +30.0%
Q12 47.0ms (34.0ms) +38.2% 25.5ms (30.0ms) -15.0% 45.5ms (42.5ms) +7.1%
Q13 221.0ms (109.0ms) +102.8% 107.0ms (105.5ms) +1.4% 58.0ms (81.0ms) -28.4%
Q14 502.0ms (133.0ms) +277.4% 130.5ms (137.0ms) -4.7% 78.0ms (108.5ms) -28.1%
Q15 370.0ms (100.0ms) +270.0% 79.0ms (100.0ms) -21.0% 58.0ms (95.5ms) -39.3%
Q16 101.0ms (102.0ms) -1.0% 90.5ms (108.0ms) -16.2% 5.0ms (25.5ms) -80.4%
Q17 487.0ms (217.0ms) +124.4% 220.0ms (212.0ms) +3.8% 64.0ms (104.5ms) -38.8%
Q18 563.0ms (206.0ms) +173.3% 213.0ms (211.5ms) +0.7% 65.0ms (107.0ms) -39.3%
Q19 895.0ms (468.0ms) +91.2% 349.0ms (412.0ms) -15.3% 94.0ms (154.0ms) -39.0%
Q20 15.0ms (12.0ms) +25.0% 3.5ms (11.0ms) -68.2% 8.5ms (24.5ms) -65.3%
Q21 550.0ms (171.0ms) +221.6% 475.0ms (170.5ms) +178.6% 289.5ms (266.0ms) +8.8%
Q22 893.0ms (160.0ms) +458.1% 623.5ms (160.5ms) +288.5% 176.0ms (339.0ms) -48.1%
Q23 1.90s (455.0ms) +316.7% 1.64s (467.0ms) +252.0% 567.0ms (733.0ms) -22.6%
Q24 24.27s (891.0ms) +2623.6% 962.0ms (898.5ms) +7.1% 644.5ms (2.51s) -74.3%
Q25 298.0ms (75.0ms) +297.3% 17.0ms (59.5ms) -71.4% 47.0ms (119.0ms) -60.5%
Q26 136.0ms (49.0ms) +177.6% 20.5ms (43.5ms) -52.9% 53.0ms (82.5ms) -35.8%
Q27 287.0ms (59.0ms) +386.4% 26.0ms (60.5ms) -57.0% 74.5ms (122.0ms) -38.9%
Q28 806.0ms (219.0ms) +268.0% 766.0ms (208.5ms) +267.4% 214.0ms (272.5ms) -21.5%
Q29 1.46s (978.0ms) +48.8% 955.0ms (995.5ms) -4.1% 411.0ms (339.5ms) +21.1%
Q30 29.0ms (30.0ms) -3.3% 22.0ms (27.0ms) -18.5% 7.0ms (21.5ms) -67.4%
Q31 624.0ms (101.0ms) +517.8% 77.5ms (99.5ms) -22.1% 76.0ms (141.0ms) -46.1%
Q32 988.0ms (100.0ms) +888.0% 132.0ms (98.0ms) +34.7% 120.0ms (149.5ms) -19.7%
Q33 313.0ms (334.0ms) -6.3% 281.5ms (328.0ms) -14.2% 9.0ms (69.5ms) -87.1%
Q34 827.0ms (416.0ms) +98.8% 463.0ms (401.0ms) +15.5% 141.0ms (265.0ms) -46.8%
Q35 847.0ms (423.0ms) +100.2% 452.5ms (400.5ms) +13.0% 140.5ms (265.5ms) -47.1%
Q36 112.0ms (120.0ms) -6.7% 94.0ms (102.0ms) -7.8% 5.0ms (25.0ms) -80.0%
Q37 320.0ms (102.0ms) +213.7% 90.5ms (95.5ms) -5.2% 30.0ms (69.0ms) -56.5%
Q38 76.0ms (53.0ms) +43.4% 30.5ms (43.0ms) -29.1% 20.5ms (23.5ms) -12.8%
Q39 298.0ms (45.0ms) +562.2% 29.5ms (47.0ms) -37.2% 19.0ms (72.0ms) -73.6%
Q40 747.0ms (189.0ms) +295.2% 245.5ms (180.5ms) +36.0% 66.0ms (125.0ms) -47.2%
Q41 23.0ms (22.0ms) +4.5% 11.5ms (18.0ms) -36.1% 8.0ms (16.0ms) -50.0%
Q42 24.0ms (18.0ms) +33.3% 10.5ms (17.5ms) -40.0% 8.0ms (15.0ms) -46.7%
Q43 21.0ms (18.0ms) +16.7% 11.0ms (15.0ms) -26.7% 7.5ms (10.5ms) -28.6%

⚠️ LiquidCache is slower on 10 queries (warm)

  • Q7: warm +inf% (1.0ms vs 0.000ms)
  • Q22: warm +288.5% (623.5ms vs 160.5ms)
  • Q28: warm +267.4% (766.0ms vs 208.5ms)
  • Q23: warm +252.0% (1.64s vs 467.0ms)
  • Q21: warm +178.6% (475.0ms vs 170.5ms)
  • Q8: warm +36.4% (7.5ms vs 5.5ms)
  • Q40: warm +36.0% (245.5ms vs 180.5ms)
  • Q32: warm +34.7% (132.0ms vs 98.0ms)
  • Q2: warm +33.3% (6.0ms vs 4.5ms)
  • Q34: warm +15.5% (463.0ms vs 401.0ms)

Compared Liquid vs DataFusionDefault on the same runner
Regressions: warm-time increases of at least 15%. Cold Time: first iteration; Warm Time: median of remaining iterations.

@anoop-narang
anoop-narang merged commit 6397c2d into main Sep 28, 2026
14 checks passed
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.

1 participant