Skip to content

Bound the parsed type AST cache - #585

Open
iliaal wants to merge 3 commits into
ClickHouse:masterfrom
iliaal:fix/type-ast-cache-bound
Open

iliaal wants to merge 3 commits into
ClickHouse:masterfrom
iliaal:fix/type-ast-cache-bound

Conversation

@iliaal

@iliaal iliaal commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #577. Only the last commit, "Bound the parsed type AST cache", is new here.

ParseTypeName() in types/type_parser.cpp keeps every distinct type name it parses in a process-wide map with no eviction. A server can send many distinct type names, one zero-row block each, and grow client memory without bound: 100,000 unique FixedString(N) names add about 72 MB RSS.

Fix: the cache holds at most kTypeAstCacheMaxEntries (4096) entries and kTypeAstCacheMaxBytes (256 KiB) of type-name text. An insert that would exceed either limit clears the cache first. The cleared map is destroyed after the mutex is released. A name longer than the byte budget is parsed on every call and never cached. Repeated types are still reused.

API change: once entries can be evicted, a raw pointer into the map can dangle in another thread, so ParseTypeName() now returns std::shared_ptr<const TypeAst> instead of const TypeAst*. The in-tree caller (CreateColumnByType, which uses auto) compiles unchanged. External code that stores the result in a const TypeAst* fails to compile, rather than silently holding a pointer with a new lifetime rule. Keeping the raw pointer would mean never freeing entries. Returning by value would deep-copy the AST for every column of every block, undoing the point of the cache.

Tests: ParseTypeName.CacheReusesAst, CacheEntryCountIsBounded, CacheByteBudgetIsBounded and OversizedNameIsNotCached.

A server-supplied FixedString(0) creates a ColumnFixedString with a
zero width, and ColumnFixedString::Size() divides by it as soon as
Block::AppendColumn() sees the zero-row header block.

A malformed width such as FixedString(-) makes std::stol throw out of
TypeParser::Parse(). ParseTypeName() inserts the cache entry before
parsing and erases it only when Parse() returns false, so the throw
leaves a half-built AST cached and the next query with the same type
hits the same division.

Number tokens now parse with std::from_chars, and invalid or
out-of-range values fail the parse instead of throwing, which lets the
existing erase path run. ParseTypeName() also parses into a local and
caches only on success, so an exception from the parser can no longer
leave an entry behind.

The factory rejects sizes outside 1..0xFFFFFF, the server's own
DataTypeFixedString limit (MAX_FIXEDSTRING_SIZE), and the
ColumnFixedString constructor rejects 0. The server never accepts
FixedString(0), so the tests that built one now use FixedString(1).
@iliaal
iliaal requested review from mzitnik and slabko as code owners October 18, 2026 16:44
The 1..0xFFFFFF FixedString check only ran in CreateTerminalColumn().
With low_cardinality_as_wrapped_column set, CreateColumnFromAst() builds
LowCardinality(FixedString(N)) straight from the AST value, so a server
could declare any width and the client would allocate it on the first
load.

I moved the check into GetFixedStringSize() and call it from both
paths.
ParseTypeName() keeps every distinct type name it parses in a
process-wide map with no eviction, so a server that sends many
distinct type names, one zero-row block each, grows client memory
without bound.

The cache now holds at most kTypeAstCacheMaxEntries entries and
kTypeAstCacheMaxBytes of type-name text. An insert that would exceed
either limit clears the cache first, and a name longer than the byte
budget is parsed on every call without being cached. The cleared map
is destroyed after the mutex is released.

Clearing would invalidate ASTs that callers are still using, so
ParseTypeName() now returns std::shared_ptr<const TypeAst> instead of
a pointer into the map. This changes a function declared in an
installed header; the in-tree caller compiles unchanged.
@iliaal
iliaal force-pushed the fix/type-ast-cache-bound branch from 7dd08c0 to cf7ea8c Compare October 18, 2026 21:16
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