Skip to content

Fix query-engine crash, name collisions, DELETE semantics and parser panics - #132

Open
austin-barrington wants to merge 1 commit into
mainfrom
fix/review-2026-11-bugs
Open

austin-barrington wants to merge 1 commit into
mainfrom
fix/review-2026-11-bugs

Conversation

@austin-barrington

Copy link
Copy Markdown
Member

Fixes the four bugs from the 0.9.4-beta review. Each was reproduced first and now has a regression test.

1. A long WHERE clause took the query engine down

SELECT ... WHERE a OR b OR ... with a few thousand terms overflowed the stack in our InfluxQL parser, which recursed once per term and was quadratic. chDB's SIGSEGV handler caught the fault and shut its engine down, so every later query failed and flushes stalled, while /health kept answering 200. Any tenant on a shared pool could do this.

  • AND/OR chains are split at every top-level operator and built as balanced trees: 4,000 terms are about 12 levels deep.
  • Expression nesting is capped at 100 levels and subquery nesting at 16, with a parse error past either.
  • /health and /health/ready return 503 once chDB reports "shutting down due to a fatal error", so Kubernetes restarts the node.

2. Different names shared one table or column

sanitise_ident maps every character outside [A-Za-z0-9_] to _ and joins db_rp_measurement. As a result, these pairs shared storage, and writes to one name overwrote the other:

  • cpu.load and cpu_load;
  • 温度 and 湿度;
  • database a with retention policy b_c, and database a_b with policy c;
  • cpu_series and cpu's series table;
  • the fields a.b and a_b.

Table names are unchanged, because renaming would orphan existing data. Instead, the client write path (single-node and cluster ingestion, line protocol and columnar) refuses a new name that maps onto storage another name owns, with a 400 NameCollision, before the WAL. Replication and shard transfers don't check, since they carry writes that were already accepted. Keys that already collide in stored data stay writable.

3. DELETE / DROP SERIES hid later writes and could break a measurement for good

Both commands stored a tombstone that every later query applied. That had three effects:

  • Points written after the delete were hidden too.
  • DELETE FROM cpu WHERE tim0 < 0 failed every later query on cpu.
  • DELETE FROM cpu with no WHERE did nothing.

Changes:

  • Physical delete: the rows are now deleted with ALTER TABLE ... DELETE (mutations_sync = 2). Each tag comparison becomes a series_id IN (SELECT ... FROM <series>) lookup.
  • WAL flush first: the delete waits for the WAL to flush (up to 15 s), so points written just before it are removed too. Points written after it stay, as in InfluxDB.
  • Validation: fields and unknown names are refused, as in InfluxDB 1.x.
  • Cluster paths: replicas (schema mutations) and shard primaries delete the rows too. A replicated predicate from an older coordinator, which uses bare tag names, falls back to a tombstone.
  • DROP MEASUREMENT removes the measurement's tombstones, so legacy tombstones can be cleared.
  • Rewrite after DROP MEASUREMENT: a measurement written again after a drop lost its fields, because the ingest schema cache still treated it as registered. MetadataPort::drop_generation now resets that cache, and the name guard, after any drop.

4. Parser panics

  • A lone ', " or / (WHERE host = ', SELECT ", FROM /, GROUP BY /) panicked the parser.
  • 5温 panicked the lexer. The lexer's duration scan is now one linear pass; it used to re-parse the input once per byte.

Found while fixing, not changed here

  • delete_series_matching ignores its predicate and removes all series metadata for the measurement. It is used by DROP SERIES and by replicated DELETEs.
  • DROP SERIES WHERE ... without FROM deletes no rows on non-sharded nodes.
  • ingest_seq is the WAL sequence plus a row offset, so it isn't monotonic across WAL entries.

Tests

cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test --lib (825), every integration binary with --no-fail-fast (compat 245, sharding, raft, e2e, …), and the CLI tests all pass locally. Cargo.lock is unchanged.

…ons, DELETE, parser panics

- A WHERE clause with a few thousand OR terms overflowed the stack in the
  InfluxQL parser (one recursion level per term); chDB's signal handler
  took the fault and shut the query engine down while /health stayed 200.
  AND/OR chains now parse into balanced trees, expression and subquery
  nesting are bounded, and /health fails once chDB reports a fatal error.
- Names that sanitise to the same table or column (cpu.load / cpu_load,
  db a + rp b_c / db a_b + rp c, non-ASCII names, field a.b / a_b) shared
  storage. The client write path now refuses a new colliding name with a
  400 before the WAL; existing tables keep their names.
- DELETE and DROP SERIES stored a tombstone every later query applied, so
  points written afterwards were hidden too, and one naming an unknown
  column broke the measurement for good. They now delete the rows
  (ALTER TABLE ... DELETE, tags as series lookups) after the WAL flushes,
  and refuse fields and unknown names. DROP MEASUREMENT clears legacy
  tombstones, and the ingest schema cache starts over after a drop, so a
  measurement written again keeps its fields.
- Lone ', " and / no longer panic the parser, and a number followed by a
  multi-byte character no longer panics the lexer (now a linear scan).
@cursor

cursor Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches production-critical paths (health probes, physical deletes, ingest validation, and replicated shard deletes); behavior changes are intentional but wide-ranging across cluster and single-node flows.

Overview
This PR hardens the node when chDB dies after a fatal error: errors from queries and Arrow inserts call note_error, and /health / readiness return 503 so orchestrators restart a process that would otherwise stay “healthy” while queries and flushes fail.

DELETE and DROP SERIES stop relying on query-time tombstones for the common case. Predicates are built for fact-table ALTER TABLE … DELETE (tag filters via series_id subqueries), the WAL is flushed first (bounded wait), and the same path runs locally and on shard primaries; older replicated predicates that reference missing fact columns still fall back to tombstones. DROP MEASUREMENT clears that measurement’s tombstones, and a new drop_generation on metadata resets ingest schema and PhysicalNameGuard caches after drops so a recreated measurement re-registers correctly.

Client writes are rejected with NameCollision (400) when a new measurement, field, or tag key would sanitize onto storage already owned by another name (before WAL append). The InfluxQL lexer/parser fixes avoid stack overflows on huge OR/AND chains (balanced trees + depth caps), panics on lone quotes/slashes and 5温, and quadratic duration lexing.

Reviewed by Cursor Bugbot for commit 7ea9643. Configure here.

@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: ab99e927-83e6-402a-b6c1-f9098ce25642)

This branch has not been deployed

No deployments
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