QuestDB 10.0.0 is a major release that introduces three key features into general availability: QWP (QuestDB Wire Protocol), a compressed binary columnar protocol over WebSockets that supersedes ILP and PG Wire for both ingress and egress; Live Views for incrementally maintaining window-function result sets with millisecond update latency; and Web Console 2.0 with Jupyter-style notebooks and coding-agent integration. Beyond the headline features, this release delivers significant performance improvements—including faster sorting, smarter partition pruning, and per-query memory limits—alongside extensive bugfixes and enhanced Parquet interoperability with tools like Iceberg, Spark, DuckDB, and Trino, making it the most reliable version yet.
Breaking Changes
This change updates what existing objects report to PostgreSQL Wire Protocol catalogue clients. Materialized views now report
pg_class.relkindas'm'(previously'r') andinformation_schema.tables.table_typeasMATERIALIZED VIEW(previouslyBASE TABLE), withis_insertable_intoset tofalse. Views now reportrelkindas'v'(previously'r') andtable_typeasVIEW(previouslyBASE TABLE), also withis_insertable_intoset tofalse. Plain tables remain unchanged. Clients that filter onrelkind = 'r'ortable_type = 'BASE TABLE'will stop listing existing materialized views and views.Every
SecurityContextimplementation must now supplyauthorizeLiveViewCreateandauthorizeLiveViewDropmethods, including implementations outside the main repository.This fix prevents compiler-internal protective double quotes from leaking into result set metadata. The SQL compiler wraps column aliases in double quotes when names contain dots (e.g., PIVOT values like
FNCL 2.5 7/26) or collide with operator tokens (e.g.,in,and), but these quotes were appearing verbatim in Web Console, PostgreSQL Wire Protocol clients, and CSV exports. The fix introducesSqlUtil.isQuoteProtectedAliasto recognize compiler-added quotes andSqlUtil.toColumnNameto strip them wherever a projection alias becomes a metadata column name. Result set column names now display cleanly:FNCL 2.5 7/26instead of"FNCL 2.5 7/26",t.x+1instead of"t.x+1", anda.binstead of"a.b". Additional follow-up fixes on this branch address a stale-quote leak after alias mutation, an infinite loop on empty PIVOT values, operator-token pivot columns breaking through joins, trailing space leaks in truncated aliases, NULL data from dotted JSON UNNEST keys, and store-reuse efficiency in alias dedup loops. This is a breaking change for clients that match column names by the old quoted format.
New Features
This feature introduces live views, which are incrementally maintained window-function results over a single WAL-backed base table, queried like a regular table. Window functions run once per row as new base commits are refreshed in, and queries against the live view scan precomputed output rather than reprocessing the base on every read. The architecture targets use cases where the same window aggregate is read frequently against high-rate ingestion, such as rolling VWAP, cumulative volume, running ranks, and day-over-day comparisons. Live views are created with
CREATE LIVE VIEWsyntax specifyingFLUSH EVERY,IN MEMORY, optionalPARTITION BY, and a requiredSTART FROMclause (NOW,BEGINNING, or an explicit timestamp literal). They support anchored windows withANCHORexpressions andDAILYsugar for cumulative aggregates that reset on a sentinel. A dedicated worker pool sized bylive.view.refresh.worker.countruns the refresh workers. The in-memory tier leads disk by the un-flushed lead, serving recent rows from RAM and older applied prefix from disk via seam-ts routing with a seqTxn consistency fence. A versioned checkpoint timeline provides durable window state with copy-on-write B+ tree indexing, and out-of-order base commits derive a finite dependency interval from the window's frame for bounded repairs. Supported window functions include ranking (row_number,rank,dense_rank), cumulative aggregates (sum,avg,count,min,max,first_value,last_value,nth_value),lag, bounded-ROWS and bounded-RANGE aggregates, Welford variance/stddev/covariance/correlation, EWMA, and the full DECIMAL aggregate family. Thelive_views()catalogue function exposes state, lag, memory, seed metrics, and checkpoint information.SHOW CREATE LIVE VIEW,EXPLAIN,pg_class,information_schema.tables,tables(), andtable_columns()all recognise the new table type.DROP LIVE VIEWandALTER LIVE VIEW RESUME|SUSPEND WALare supported for lifecycle management.This feature adds opt-in memory limits that cap how much native memory a single bounded workload may allocate, throwing at the offending allocation site when the cap is crossed so a runaway is stopped at its source while unrelated workloads keep running. Three independent limits are provided:
cairo.query.memory.limit.bytesfor user SQL queries,cairo.mat.view.refresh.memory.limit.bytesfor materialized view refresh, andcairo.wal.apply.memory.limit.bytesfor WAL apply. All default to 0 (unlimited) and are dynamically reloadable viareload_config(). AMemoryTrackerwraps a 16-byte native block shared with Rust, acquired per workload viaQueryRegistry.register/unregister. Tracker-awareUnsafe.malloc/realloc/freeoverloads update both the global RSS counter and the per-query block. Wired sites include map family, sort/tree chains, hash-join chains,FastGroupByAllocator,LATEST BYrowid lists and maps, set-operation maps, encoded sort, window-join and horizon-join aggregations,SAMPLE BYfill, and parquet decode buffers. Thequery_activityview gainsmemory_usedandmemory_limitcolumns. On breach, the engine throws aCairoExceptionwithisOutOfMemory()set and a message identifying the workload type, query ID, limit, and memory tag. Coverage is best-effort; structurally bounded operators and the vectorized Rosti keyedGROUP BYhash tables remain on the global counter only.This feature adds controls for managing hard-suspended WAL tables and a new
ALTER TABLE <t> REBASE WALstatement to rebuild a table under a fresh sequencer. Thecairo.wal.apply.suspended.tablesconfiguration property is a reloadable, comma-separated list of table names thatApplyWal2TableJobskips, preventing WAL transactions from being applied. Whencairo.wal.apply.suspended.write.deniedis set to true, writes to a hard-suspended table are rejected instead of being queued. Non-structural ALTERs andFORCE DROP PARTITIONare routed through the WAL-bypass path so they apply directly viaTableWriter, while structural changes remain denied.ALTER TABLE SUSPEND WALandRESUME WALauthorize through the sameSecurityContext.authorizeResumeWalpath, making them symmetric.REBASE WALauthorizes separately throughauthorizeRebaseWal, which defaults toauthorizeSystemAdmin(). The rebase operation clones the applied table into a new directory via hard links into a.rebase/staging directory, resets_txnand_metawith a newtableId, seeds two empty transactions, swaps the name registry, and drops the old directory. Rebasing a base table invalidates dependent materialized views, while rebasing a materialized view itself re-registers it for a full refresh. Preconditions require the table to be a WAL table, hard-suspended, and have write-deny enabled. Force-applied changes bypass the WAL and are not propagated to replicas.This feature enables
ALTER TABLE ... ALTER COLUMN ... TYPEto work on tables containing parquet partitions. Previously, the operation supported native partitions only and either failed or silently left parquet data unconverted. Conversion is lazy: parquet partitions are not rewritten by the ALTER, and decoding cost moves to the query path with a one-time decode per row group buffered for subsequent rows. O3 merges that land on a converted partition rewrite it with the new type. Conversions involving SYMBOL and chained conversions where the parquet column type no longer matches current metadata eagerly convert affected parquet partitions to native before the ALTER. EachALTER COLUMN TYPEcreates a new column index with areplacingIndexlink to the previous index, andO3PartitionJobandPageFrameMemoryPoolwalk this chain to map current writer indexes to columns inside older parquet files. The query path carries a per-column conversion strategy and converts at accessor level using pooled sinks for zero-GC operation. Fixed-to-fixed conversions happen in the Rust decoder viapost_convert(). Null sentinels, BYTE/SHORT/CHAR column tops, date/timestamp scaling, and UTF-16/UTF-8 transcoding all match the native partition behaviour.This improvement moves covered-index column decode for aggregation and filter queries off the single dispatch thread onto the async reduce-worker pool, eliminating the dispatcher serialization that made the covered plan 3-4x slower than necessary. The covered decode now parallelizes 3-4x, and for selective predicates the covered plan is significantly faster than a full scan.
count(*) WHERE sym = '<lit>'is answered from posting-index metadata with no decode at all, using O(genCount)countMatchesClampedandselectKthMatchrandom access into the posting index. ACoveredColumnDecoderwith per-poolCoveringBuffersdecodes covered columns from the sidecar and synthesizes the symbol key on both flyweight and record fast-paths, supporting all column types including variable-size (VARCHAR/STRING/BINARY/ARRAY). Concurrent-read-safe posting readers use detached per-worker cursors with a freeze before dispatch so workers never mutate shared reader state. Projection and null-pad wrappers propagate covered metadata through their column remap. Benchmarks show 2.6-3.8x speedup from parallelization on decode-heavy shapes, and at 0.1% selectivity the covered plan is 15-19x faster than a full scan for aggregations and 614x faster forcount().This feature provides a schema-only logical dump that emits round-trippable QuestDB DDL for every user object in the database, one statement per row. Since
pg_dumpcannot run against QuestDB due to absentpg_*catalogs and QuestDB-specific DDL (PARTITION BY,SYMBOL CAPACITY,timestamp(),DEDUP,TTL,IN VOLUME,BYPASS WAL, storage policy), this command emits native QuestDB SQL instead. Objects are emitted in dependency order: tables first, then materialized views (whose base tables are already emitted), then views (topologically sorted via the view graph so view-on-view replays succeed). The command supportsINCLUDE/EXCLUDEfiltering by category (TABLES,VIEWS,MATERIALIZED_VIEWS,SCHEMA,ALL). Concurrent DDL during the dump no longer fails the entire operation — objects dropped between the initial snapshot and their emit time are skipped with a logged warning rather than causing an error.This feature enables
<,>,=,<=,>=,!=, and<>comparisons between numeric columns and single-column scalar sub-queries. Supported left operand types includeDOUBLE,FLOAT,LONG,INT,SHORT, andBYTE. For example:SELECT * FROM trades WHERE price > (SELECT avg(price) FROM trades WHERE timestamp IN today()). The sub-query is evaluated once per query execution and the result is cached. Integer operands compared against integer cursor values uselongcomparison to preserve precision beyond the exact integer range ofdouble. Zero rows produce no match, one row supplies the cached scalar value, and more than one row raises an error. Only the<,>, and=factories are registered explicitly —FunctionFactoryCachederives the remaining operators and argument-swapped forms automatically. The implementation includes proper worker state donation for parallel execution paths including async filters, parallelGROUP BY, window joins, and horizon joins.This feature adds statistical aggregate functions for kurtosis and skewness analysis:
kurtosis()/kurtosis_samp()for sample excess kurtosis,kurtosis_pop()for population excess kurtosis,skewness()/skewness_samp()for sample skewness, andskewness_pop()for population skewness. For example:SELECT skewness(price), kurtosis(price) FROM trades WHERE symbol = 'BTC-USD' SAMPLE BY 1h. The implementation uses Pébay's one-pass online algorithm (an extension of Welford's), maintaining running mean and central moments (M2, M3, and M4 for kurtosis) plus a count per group. Partial aggregates merge with pairwise-combine formulas, enabling parallelGROUP BYexecution. Sample skewness returnsnullfor fewer than 3 values, sample kurtosis for fewer than 4, and all variants returnnullwhen every observation is equal (zero variance).This feature introduces two new SQL functions built on a worker-continuation framework using
jdk.internal.vm.Continuation. While these functions are parked, the worker carrier is released to run other queries, so the number of concurrent calls is no longer bounded by the worker pool size.wait_wal_table(table_name [, seq_txn])blocks the current query until the WAL writer for the specified table has applied transactions up to a targetseq_txn, returningtrueon success. Whenseq_txnis omitted, it captures the table's current sequencer transaction at start time. It throws if the table becomes suspended or is dropped, and respects the SQL circuit breaker for timeout and cancellation.sleep(seconds)pauses the current query for the given duration (up to 24 hours) and emits a singleTIMESTAMProw containing the server's wall-clock time at wake. Both functions respect query timeout,CANCEL QUERY, and broken client connections. The continuation runtime includes WAL commit notification to fire waiters as soon as the target transaction lands, and aTimerShardsdaemon pool for periodic wakeups. New configuration properties includecairo.timer.shards(number of timer daemon threads, defaults tomin(4, max(1, cpu_count / 4))) andgriffin.query.continuation.wake.interval(millisecond interval for circuit breaker probing during park, default 1000).This feature introduces a first-run onboarding experience for Notebooks in the Web Console. A two-step onboarding modal guides users through Notebooks capabilities (organizing queries, results, and charts into dashboards) and introduces the QuestDB MCP connection with a copy-ready setup command and an animated agent transcript. An in-notebook MCP promotion card is surfaced inside notebooks across list, grid, and empty states, and can be collapsed or dismissed. Visibility is driven by a
notebook.onboardinglocalStorage blob with flags for MCP connection status and promotion display, and all onboarding surfaces are permanently suppressed once an MCP bridge has connected.Agents can now edit any notebook in the background without stealing focus from the user. This feature introduces a Dexie-based controller for queued operations on unmounted buffers, a per-buffer FIFO queue to strictly order all writers, and a mount-claim protocol that handles conflicts when a user opens a buffer mid-edit. Headless cell execution runs cells on unmounted notebooks with a verification outcome machine covering scenarios such as notebook deletion, user mounting mid-run, and cell changes mid-run. Freshness gating rejects mutating tools with
STATE_STALEif the user edited the notebook after the agent's last read. Anactivate_notebooktool brings a buffer to the foreground on user request. Footer notifications surface agent edits to non-active buffers as an unread-style indicator on the MCP pill with a transient popper and a persistent row in the pair popover.This feature reworks the notebook cell experience with a width-aware toolbar that adapts to the cell's width across compact, standard, and expanded layouts. A table-to-chart view toggle transfers existing data between the grid and chart without re-running the query. Inline cell rename makes the cell name the single source of the chart title, with automatic migration from the old chart config name on load. Per-cell auto-refresh supports adaptive, off, or fixed intervals (1s, 5s, 10s, 30s, 1m). MCP bridge version awareness detects version mismatches — a major mismatch blocks with an upgrade command, while a minor one connects but warns, surfaced in the pairing popover, consent modal, and footer status pill. Left and right sidebars now auto-close on shrinking window sizes.
This feature introduces a cell-based notebook surface in the Web Console where users can mix SQL, markdown, and chart cells in a single notebook, each independently runnable. Query results render in a virtualized grid with support for multi-statement cells split across result tabs, row-count/timing status, truncation indicators, and one-click CSV/Parquet download. Charts support line, area, bar, stacked bar, scatter, pie, and candlestick types rendered with ECharts, with auto-inference of chart type and encodings from results. Notebooks support
DECLARE-style@variablesreusable across cells, drag-to-reorder cells, focus mode, resizable grid/column layout, and full-text search spanning notebook cells alongside editor tabs. Notebooks persist locally via IndexedDB and survive reloads. A new MCP bridge enables external AI coding agents (Claude Code, Codex, Cursor, OpenCode, etc.) to connect to the running Web Console over a local WebSocket and operate it. Agents must go through explicit pairing via a consent modal showing what is connecting and what it can do. Granular, revocable permissions (read/write/schema scopes) default to read-only, and SQL that cannot be safely classified is treated as a write and denied. Each bridge run generates a fresh port and pairing token held only in memory. The AI assistant provider layer supports Anthropic, OpenAI (both Responses and Chat-Completions protocols), and custom providers.
Improvements
This improvement routes WAL commit blocks that are a pure append to the last native partition through the existing fast-lag append path, writing covered fragments incrementally (O(new rows)) instead of re-encoding the entire partition's covered sidecar (O(partition)). Per-block covered-index cost drops from O(partition) to O(new rows) on the common path, making WAL drain approximately 86× faster in benchmarks. The change also fixes a pre-existing concurrency bug in covering indexes shipped since 9.4.0, where a chain entry's cover-end footer could be clobbered during append, causing concurrent covered readers to read garbage and potentially crash with an out-of-bounds read. The fix de-aliases the footer to a fixed offset under a versioned layout for all new data, with automatic COW-migration of existing format-0 heads on first extend.
This improvement extends the encoded radix sort to every column type that
ORDER BYaccepts and to keys of any width, across both full-sort and serial/parallel top-K paths. Previously, the encoded sort only engaged when allORDER BYcolumns were fixed-width and the combined key fit 32 bytes — everything else fell back to the red-black tree sort with O(log n) comparator calls per row.VARCHARkeys use UTF-8 bytes with a terminator,STRINGand non-staticSYMBOLuse UTF-16BE with escaping, andUUID/LONG256use unsigned big-endian with order-preserving null remapping. Variable-width keys use a 16-byte inline prefix with overflow into a key heap; rows whose prefix already exceeds the kept boundary in top-K queries skip the full key encode entirely. Benchmarks on the ClickBenchhitstable show improvements from 52ms to 37ms atLIMIT 10and from 100ms to 85ms atLIMIT 100000. The key heap is bounded bycairo.sql.sort.key.max.bytes, and the tree sort remains available via thecairo.sql.orderby.sort.enabledkill switch. This change also fixes a pre-existing bug wheregenerateCastFunctionshad noLONG128case, which could causeUNIONqueries with bothSYMBOLandLONG128columns to throw an error.Previously, wrapping the designated timestamp in any function (e.g.,
date_trunc('day', ts),CAST(ts AS TIMESTAMP),dateadd('h', 1, ts)) defeated partition pruning and fell back to a full table scan with a row filter. This improvement lets the optimizer push such predicates back onto the timestamp axis when the wrapping function is monotonic. A predicate is recognized, the queried value range is inverted layer by layer back to a range on the designated timestamp, and the result drives the usual interval scan and partition pruning. Functions are graded as EXACT (inverse is exact, row filter dropped), SUPERSET (inverse soundly contains the true preimage but may be wider, interval prunes but original predicate stays as residual filter), or NONE (cannot bound, predicate stays a plain row filter). Covered functions includeCAST,date_trunc,timestamp_floor,timestamp_ceil,dateaddwith fixed units,year,to_timezone,to_utc, and chains of these functions. Constant bounds are inverted at compile time; runtime bounds (now(), bind variables) are deferred to evaluation when the scan opens. Recognition is fail-safe: any compile or parse failure falls back to a row filter, so queries that did not previously use this path are never broken. This improvement also fixesgetNextDSTbehavior before a zone's first transition, adds overflow hardening for fixed-unitdateaddandtimestamp_ceilinverses, and introduces domain-ceiling-aware overflow checks for constant-shift inverses.Subexpressions whose value is invariant across the rows of a single cursor but not a compile-time constant were previously re-evaluated for every row when appearing inside per-row contexts such as
CASEbranches or filter predicates. For example, inSELECT sum(CASE WHEN ts >= dateadd('d', -30, to_timezone(now(), 'Asia/Kolkata')) THEN amount ELSE 0 END) FROM trades, theto_timezone(...)anddateadd(...)calls were repeated on every row despite producing the same result for the whole scan. This improvement wraps such subtrees in aRuntimeConstFunctionthat evaluates once duringinit(), caches the result in a primitive field, and serves it from every getter. The fold is applied at function boundaries only, wrapping the topmost runtime-constant node without nesting. Trivial runtime-constant leaves like bind variables andnow()that already cache their values are skipped to avoid unnecessary indirection. Only fixed-width scalar types are folded; variable-length types are left untouched. The wrapper delegatestoPlan()to its argument, soEXPLAINoutput is unchanged.Parallel
GROUP BY,TOP K, and ASOF/window joins hold per-queryAsyncFilterContextinstances containing one-per-workerDirectLongListbuffers for matched row IDs. Under a JIT filter these lists were pre-sized to the full page frame (up to 8 MB each at the default 1M rows) and only ever grew. Previously,clear()at cursor teardown freed page-frame memory pools but left these lists at their peak size, freeing them only on query-cache eviction. This meant idle or cached factories kept1 + workerCountpeak-sized buffers allocated for their entire cached lifetime. This improvement now callsresetCapacity()on the owner and per-worker lists duringclear(), returning them to the 256-long initial capacity. Idle factories no longer pin peak-sized row-id buffers. The tradeoff is that a cached factory reused for another large scan pays one reallocation to grow back. This improvement also fixes a native memory leak whereresetCapacity()could re-malloc a closed list whenclear()was called afterclose()on the horizon-join factories' failedgetCursor()path, and reorders the 4 horizon-join factories to free the cursor before the frame sequence to prevent this scenario.This improvement makes two independent optimizations to checkpoint/snapshot restore. For tables with many Parquet partitions, each partition's parquet-metadata sidecar (
_pm) is now mapped and checksum-verified once instead of up to twice mapped and three times verified, and the previously serial per-partition validation now runs in parallel across the recovery pool. Workers pull parquet partitions from a shared cursor with dynamic load balancing, reusing a single set of native scratch objects across every partition they handle. The committed-size truncation check ondata.parquetruns inside the worker before regeneration, removing the O(N) serialff.length()loop. For tables with few partitions but several indexed symbol columns, the native bitmap-index rebuild now parallelizes across individual(partition, column)work items rather than whole partitions, so a non-partitioned table no longer serializes all indexed columns onto one thread. A behavioral tradeoff is that truncated capture detection now occurs inside parallel workers rather than strictly before sibling work starts, but the first failing worker trips a shared abort latch that causes all other workers to short-circuit at their next item boundary.This improvement makes large notebooks remain responsive as they grow by virtualizing heavy cell components. Cells outside the viewport no longer keep their full editor, chart, result grid, and refresh workload active, reducing render work, memory use, and background queries. Nearby cells render normally while distant cells use lightweight placeholders. Focused, running, and recently edited cells stay active, and full content returns when the user scrolls back. Offscreen chart refreshes pause and resume when needed, so notebooks with many live charts do not continuously query and render in the background. Saved query results can be released from memory and loaded again on demand while cell sizing and run status remain available. With these optimizations in place, the notebook cell cap increases from 50 to 200.
This improvement replaces the 943 KB login-background.svg (a Figma export with every glyph outlined as vector paths) with a ~7 KB React component that renders the same binary-rain text using the theme's monospace font and a CSS gradient mask. This reduces the Web Console's distribution size by ~950 KB and the background's transfer size from ~44 KB (gzipped) to ~2 KB, while keeping the text crisp at any resolution.
This improvement extracts the result grid into a self-contained, framework-native React component built on TanStack Table (columns, pinning, ordering) and TanStack Virtual (row and column virtualization for performant rendering of wide, deep results). The grid owns grid-only concerns: cell/keyboard navigation, copy cell and column, freeze-left, move-column-to-front, reset layout, debounced column resize, scroll shadows, and markdown export. Data is supplied through a pluggable
ResultGridDataSourceinterface with two implementations:inMemoryDataSourcefor bounded, fully-loaded results andusePagedDataSourcefor server-paged unbounded results with sparse page cache, debounced fetch windows, outlier-page purging, and a generation guard preventing stale pages from applying to current queries. AResultGridAdapterimplements the legacyIQuestDBGridinterface so existing call sites remain untouched. Column layout persistence (sizing, order, pinning) is preserved viacolumnLayoutStore. The new grid is enabled by default behind auseNewGridfeature flag, with the legacygrid.jspath available as a fallback viauseNewGrid=falseor the?useNewGrid=query parameter.
Bug Fixes
The four VWEMA window functions held
args.get(3)in a field of their own and read it viagetDouble, but never propagatedinit,initPartitionBy,toTop, orcursorClosedto it. A volume sub-expression that resolves state ininit()therefore ran unbound. For example, inavg(price, 'alpha', 0.5, CASE WHEN side = 'BUY' THEN qty ELSE 0.0 END) OVER (PARTITION BY sym ORDER BY ts), the symbol comparison resolves its constant ininit(), so un-initialised it reported "no such symbol" for every row, causing the CASE to weight every row 0.0 and the function to read NULL throughout instead of the volume-weighted average. All four classes now propagate all four hooks.This fix addresses an issue where
WindowContextImpl.of()converted a frame bound into the designated timestamp's units throughTimestampDriver.from(long, char), which checked neither the multiply for overflow nor the int narrowing that followed. Over a nanosecond designated timestamp,RANGE 200000 DAY PRECEDINGwrapped onto a positive value and was refused as a FOLLOWING frame start,RANGE 300000 DAY PRECEDINGwrapped negative and silently returned rows over a 236-year frame where 821 years were asked for, andRANGE 4294967296 DAY PRECEDINGnarrowed to exactly 0. All three drivers now range-check the conversion and report the offending bound at its own position with a human-readable message including the unit name.A page-frame cursor's fast skip path counted physical partition rows across row groups that parquet-level pushdown pruning had already discarded, so a resumed skip landed short and re-fed already-consumed rows.
PageFrameCursor.hasActivePushdownFilter()now routesskipRowsto the row-by-row path while pruning is active.Making pushdown a third trigger for the row-by-row path exposed a defect where the walk advanced through
hasNext(), which charged every row against the post-skip decode clamp. A clamp of 0, which is whatLimitRecordCursorpasses to size a cursor it will not read, made the walk skip nothing. This causedcalculateSize()to report 0 rows for queries likeWITH cte0 AS (SELECT * FROM t WHERE c2 IS NOT NULL LIMIT 16) SELECT * FROM cte0 LIMIT 40. The clamp now stays off while the walk runs and arms once the skip lands.On rehydrate, a materialized view created but never refreshed before the demote came back valid and empty with watermark -1, and nothing kickstarted it.
buildViewGraphsnow enqueues the initial incremental refresh for a never-refreshedREFRESH_TYPE_IMMEDIATEview, mirroring the kickstart the persisted-state path already performs.This fix addresses an issue where
PushdownFilterExtractorpushed both null operators onto parquet row-group pruning for every column type, but a parquet null bit and a SQL NULL denote the same rows only for some types. BOOLEAN, BYTE, and SHORT carry no null sentinel, so a row group built entirely from column-top rows reportednull_count == num_valuesand pruning discarded it forIS NOT NULLeven though every one of its rows matched. CHAR, FLOAT, and DOUBLE fail the other way because their SQL NULL is broader than what the writer marks null. The extractor now refuses only the unsound direction per type.This fix addresses an issue where
isOlderThanTtlconverted an hours-based TTL with an unchecked multiply. Values up toTTL 2147483647 HOURScould reach the conversion against a ceiling of 2562047 hours for nanosecond timestamps.TTL 106752 DAYS, the smallest DAYS value that trips it, wrapped pastLong.MAX_VALUEinto a negative span that every partition compared older than, so the next commit dropped every non-active partition silently. A TTL the timestamp type cannot express now expires nothing, guarding at the point of use rather than at the DDL to coverCREATE TABLE,ALTER SET TTL,CREATE TABLE LIKE, restored metadata, and tables written by older binaries.The four
WINDOW JOINbound conversions calledTimestampDriver.from(long, char)directly with the same unchecked multiply and int narrowing as the plain RANGE frame. On a nanosecond designated timestamp,RANGE 300000 DAY PRECEDINGwrapped onto 86496 days andRANGE 4294967296 DAY PRECEDINGnarrowed to exactly 0. Two of the four sites fed converted bounds intointrinsicModel.mergeIntervalModel, so a wrapped bound narrowed the slave scan interval and dropped rows before evaluation.WindowContextImpl.toTimestampUnitsbecomes the single shared guard and all four sites call it. Queries whose bounds previously wrapped silently now fail to compile.A client that was writing into a materialized view via QuestDB Wire Protocol and seeing no error will now receive an error. Previously, such writes were silently dropped.
A snapshot or checkpoint could capture a parquet partition whose on-disk
data.parquetis a later generation than the committed parquet size recorded in_txn. The restore path trusted the partition's existing_pmsidecar in that state and left it stale, so the first read or merge after restore decoded a column chunk that lies past the committed size, resulting in aFile out of specificationerror. On a replica that replays the WAL over the restored partition, theApplyWal2TableJobapply fails and the table is suspended, breaking replication. This fix only trusts an existing_pmwhendata.parquetis exactly the committed size. When the file is longer (the snapshot captured the partition mid in-place rewrite),_pmis regenerated from the committed size. Regeneration fires only for genuinely torn partitions and is correctness-preserving, rebuilding a compact_pmfor the committed footer.This fix addresses a production out-of-memory incident on a memory-constrained instance ingesting a skewed symbol column. The WAL fast-lag commit fires an inline seal once the generation count reaches
MAX_GEN_COUNT. The incremental seal branch sized its per-stride merge/trial buffers as if all 256 keys in a stride held the single hottest key's row count, inflating the allocation by up to ~256x beyond the data the stride actually merges. The incremental seal now sizes its buffers to the actual aggregate of the dirty strides, and a pre-flight defers to the full seal when even the correctly sized buffers would breach the RSS limit. The rollback path was also reworked to stream per key for every cutoff shape, bounded by the largest single key instead of the whole index, eliminating the whole-index decode that was the memory-heavy case. Additional hardening includes: poisoning the writer on post-switch failures so mutating entry points cannot re-drive publishing at a staged seal transaction, bounding native writes in posting decoders against corrupt or torn input to prevent out-of-bounds writes, and adding a live-head check to the seal-purge operator to prevent deletion of live files after a seal transaction is freed and reused.Two malformed expressions involving
CASE ... ENDcrashed the query with an internal error instead of returning a syntax error. A binary operator with a missing right operand directly after aCASE ... ENDexpression (e.g.,SELECT SUM(CASE WHEN true THEN 1 ELSE 0 END & )) crashed with aNullPointerExceptionbecause the expression parser'sENDkeyword handler left a stale depth counter of 1, which defeated the arity guard and allowed a null left-hand side to be assigned. The fix resets the depth counter to 0 after the flush loop so the arity guard fires correctly, producing a clear error liketoo few arguments for '&' [found=1,expected=2]. A dot directly afterCASE ... END(e.g.,SELECT CASE WHEN true THEN 1 ELSE 0 END.foo) also crashed because the dot handler attempted to cast the top of the operator stack to aFloatingSequenceliteral, which failed with aNullPointerExceptionorClassCastException. The fix extends the token only when the stack top is aFloatingSequenceliteral and otherwise raises a syntax error. Valid queries produce the same parse as before; only the previously-crashing inputs change behavior.This fix addresses a storage-engine corruption that could occur when a WAL
REPLACE_RANGEcommit in O3 mode appends brand-new partitions above the table's previous last partition. InTableWriter.processO3Block, replace mode derivedpartitionTimestampHifromo3TimestampMax, which is the replace-range high boundary rather than the highest timestamp actually written. For an open-ended range the boundary isLong.MAX_VALUE - 1, andgetCurrentPartitionMaxTimestampoverflows on it. As a resultpartitionTimestampHistayed at the stale pre-commit ceiling,finishO3Commitskipped switching the writer's active column files to the new last partition, and the next commit reused the previous partition's column descriptors, producing rows dated below the new partition's floor or suspending the table. The fix derivespartitionTimestampHifrom the actual highest written timestamp in replace mode. The non-replace path is unchanged, and there is no measurable performance impact.A
SAMPLE BYquery that aggregates withfirst()/last(), filters on an indexedSYMBOLcolumn, and usesALIGN TO FIRST OBSERVATIONcould return a bucket timestamp where an aggregated value was expected. The wrong results appeared only when the designated timestamp column was left out of theSELECTlist, and only for the buckets in the middle of the result. The first and last buckets were always correct, which made the problem easy to miss. There was no error and no crash, so an affected query silently produced a plausible-looking timestamp in place of the value. The root cause was thatSampleByFirstLastRecordCursorFactoryemitted the middle (non-boundary) rows through its data record, which special-cased the bucket-timestamp column using the base-table timestamp index rather than the projection index. The two indexes coincide only when the timestamp is projected at the same position it occupies in the base scan. When the timestamp was not projected, the base index pointed at an unrelated projected column and overwrote it with the bucket timestamp. The fix makes the data record use the same projection index as the boundary record. Adding the timestamp to the projection, switching toALIGN TO CALENDAR, or dropping the indexed-symbol filter all avoided the problem because each routes the query away from the affected execution path.This fix introduces three rounds of enhancements to the seeded query fuzzer and addresses 25 distinct defects surfaced by them. Cursor self-consistency checks verify that re-iterating after
toTop()reproduces the result set, thatpreComputedStateSize()remains unchanged, and thatsize()andcalculateSize()agree with the materialized row count. Fault injection arms filesystem, native allocation, and function faults on a fraction of queries to exercise error-path cleanup. Key engine fixes include:SAMPLE BY FILL(LINEAR)cleanup on out-of-memory no longer throwsNullPointerException; covering posting index crashes on sidecar I/O errors now propagate the real failure;calculateSize()for backward interval scans now mirrorsnext()for correct row counts; DATE-argument window value functions (max,min,first_value,last_value,nth_value,lag,lead) no longer throwUnsupportedOperationException;rank()anddense_rank()no longer crash when a pass-through column of an unserializable type (UUID, STRING, VARCHAR, LONG256, BINARY, array, INTERVAL) is projected;lead()over high-precision DECIMAL no longer crashes; windowRANGE-frame functions now use the correct designated timestamp index when the timestamp is not in the projection;sum()andavg()over Decimal256 sliding frames now evict values with scale-agnostic subtraction; TopK /ORDER BY ... LIMITcleanup no longer crashes when a tree-chain allocation fails mid-construction; JIT filter now correctly widens nested INT products to 64 bits when a FLOAT operand is present; keyed ASOF JOIN sink-heap no longer leaks on out-of-memory during cursor open; vectorized rostiGROUP BYno longer over-freesNATIVE_ROSTImemory onwrapUp()resize; stale aggregate tasks in the shared vector-aggregate queue no longer crash later queries;GROUP BYwith a trivial arithmetic expression key referenced by alias now compiles correctly; multi-key index scans no longer leak posting-index block buffers on out-of-memory; and window functions over untypednullliterals now raise a cleanSqlExceptionsuggesting a concrete cast instead of behaving non-deterministically across platforms. APageFrameAddressCache.of()now reopens lists atomically so a failed reopen leaks nothing.When a non-WAL writer ingests out-of-order data and the in-order prefix crosses a partition boundary, the partition switch sealed the earlier partition in memory but did not persist
_txn. On the next lag commit,o3MoveUncommittedreclaimed the active partition's uncommitted in-order tail into the O3 buffer and resetmaxTimestampto the durable_txnvalue, which predated the switch. This caused the high-water mark to end up below committed data, so a rollback reloaded it and the next O3 commit reordered the partition tail. This fix introduces a new variable to track the max timestamp of the last sealed partition, updating it whenever a partition switch happens, ensuringmaxTimestampis never reset below the actual committed maximum.This fix resolves an intermittent
NullPointerExceptionthat occurred under concurrent queries when using a window function over aUNION ALLwhose first branch was an IN-list scan served by a posting/covering index. The root cause was thatCoveringIndexRecordCursorFactorystored a direct reference to the compiler's pooledkeyValueFuncslist fromIntrinsicModel. SinceSqlCompilerinstances are pooled and shared across threads, another thread borrowing the same compiler could clear the list between compile and execute, nulling out slots that the first thread'sgetCursor()then attempted to read. The fix creates an owned copy of the list in the constructor, preserving the sameFunctioninstances that the factory already owns and frees on close.Subtracting a
CHARorSTRINGliteral from a timestamp (e.g.,SELECT * FROM t WHERE timestamp > now() - '1' LIMIT 1) failed with an internal error. TheSubTimestampFunctionFactoryread the right operand withgetTimestamp()instead ofgetLong(), and callinggetTimestamp()on aCharFunctionthrewUnsupportedOperationException. The crash also occurred on the interval-optimizer path viaRuntimeIntervalModel. This fix changes the factory to read the right operand withgetLong(), mirroring the symmetricAddLongToTimestampFunctionFactorywhich already did this correctly. As a result,-and+now produce identical, well-typed cast errors for non-numeric literals.This fix resolves a data-ordering bug in the non-WAL out-of-order (O3) commit path where a lag commit could leave the table's
maxTimestampbelow the maximum timestamp actually committed to disk. When uncommitted rows of a lag commit spanned more than the active partition,o3MoveUncommitted()pulled only the active partition's rows into the sorted O3 batch. The in-order rows left on disk in the previous partition stayed committed but were not part of the sorted O3 batch, somaxTimestampwas recomputed solely from the O3 batch boundary and could end up lower than the true on-disk maximum. A later O3 commit then merged against that stale boundary and produced a single timestamp inversion. The fix reads the previous partition's actual max timestamp viareadPartitionMinMaxTimestamps()and folds it into the committedmaxTimestampcomputation when the active partition is emptied while uncommitted rows remain in an earlier partition.Equality and
INfilters on a renamed column of a Parquet partition could return too few rows (often zero), silently dropping valid data. The defect was in Parquet row-group bloom-filter pushdown, which resolved the filtered column against the Parquet file's column names. Those names are frozen when the partition is converted to Parquet, so a laterRENAME COLUMNleft them stale. When another column already carried the query's current name (common after a chain of renames), the pushdown landed on the wrong Parquet column, checked that column's bloom filter, got a false negative, and skipped the entire row group. This fix makes the native-table pushdown resolve columns by stable column id, the same way the data-decode path already does.read_parquet()cursors retain name-based resolution, which is the correct semantics for arbitrary external files without QuestDB column ids. Pruning effectiveness is unchanged for the common non-renamed case, and the fix has negligible cost sincegetColumnIndexByIdis a linear scan run once per partition during filter-list preparation.Many SQL query factories only consulted the execution context's circuit breaker inside their row-processing loop, which never runs for queries that produce no rows (empty tables, no-match filters, empty joins/aggregates), for instant single-row results, or for parallel paths that dispatch nothing when the input is empty. These queries ignored
CANCEL QUERYand query timeouts entirely. This fix adds breaker consultation at the point work begins across all affected factories — at the top ofhasNext()for joins and latest-by cursors, before build/aggregate loops for group-by and window functions, at cursor open for empty-cursor singletons, and before dispatch on parallel paths. A new time-throttled breaker variant always tests cancellation and timeout (both cheap, no syscall) while throttling only the heavy connection probe syscall by elapsed wall-clock time, defaulting to a 100ms window. Every query now pays for one real breaker check on its first consultation, including a singlerecv(MSG_PEEK)connection-probe syscall. This is a behavioral change: a query that previously ran to completion under an aggressive timeout can now abort, including catalogue and admin listings.The incremental posting-index seal took an unguarded covered-column sidecar snapshot into native memory before the RSS pre-flight ever ran, so a skewed symbol with a covered (
INCLUDE) column could still trigger an OOM and suspend the table. On a skewed symbol, a single partition's covered sidecar could be multi-GB, and the eagermalloctripped the global RSS limit during a WAL fast-lag commit. This fix adds the same headroom pre-flight before the snapshot copy, re-reading RSS memory usage per cover so it accounts for snapshots already taken in the loop. When the copy would not fit the live headroom, the incremental candidacy is cleared and execution falls through to a full seal, which rebuilds every sidecar by streaming per-key from the column files and needs no snapshot, with peak memory bounded by the largest single key rather than the entire sidecar.When a file's length could not be read during metadata loading,
MemoryCMRImpl.of()readerrnoafter a cleanupclose()call, which could overwrite theerrnoleft by the failedlength()via a nativeclose()syscall. Since metadata reload classifies failures byerrnoto decide whether to retry, a clobberederrnocaused transient "file does not exist" conditions — such as a_metafile briefly unreadable during a concurrent metadata change — to be misclassified as fatal. Instead of retrying until the spin-lock timeout and reporting a clean "Metadata read timeout," the lower-level "could not get length" error surfaced. This fix captureserrnoimmediately after the failedlength()call, beforeclose()can overwrite it.A materialized view's incremental refresh could fail transiently when the base-table reader pool was exhausted ("table busy") or when a refresh query ran out of memory. Previously both were treated as fatal — the view was invalidated immediately, and a busy failure cascaded up the dependent-view chain. This fix routes transient errors through a non-blocking, capped, timer-driven retry path. On transient failure, a per-view backoff deadline is scheduled and the view reports a new
retryingstatus queryable viamaterialized_views.view_status. Each consecutive failure that does not advance the refresh watermark bumps a counter, and once it exceedscairo.mat.view.refresh.busy.retry.limit(default 10) the view is invalidated, bounding WAL retention. The counter resets on any successful refresh. Additionally, a newcairo.mat.view.refresh.block.listconfiguration option accepts a comma-separated list of materialized view names that the refresh job must never refresh, serving as an escape hatch for views whose refresh crashes or destabilizes the database. Blocked views are skipped without being invalidated. The OOM path no longer callsOs.sleepbetween in-call step-halvings, avoiding blocking the single refresh worker that drains the entire task queue. New configuration properties:cairo.mat.view.refresh.busy.retry.timeout(default 1000ms),cairo.mat.view.refresh.busy.retry.limit(default 10), andcairo.mat.view.refresh.block.list(default empty).The Pearson correlation denominator
sqrt(sumXX * sumYY)could overflow to+Infinityfor large-magnitude inputs (near+/-1e153) or underflow to0.0for small-magnitude inputs (near+/-1e-150), producing incorrect results of0.0orNaNrespectively. This fix prefers the single-roundingsqrt(a * b)path when the product is finite and non-zero, and falls back tosqrt(a) * sqrt(b)when overflow or underflow is detected. The final Pearson result is clamped to[-1, 1]to absorb small rounding drift in the fallback path. The fix applies toCorrGroupByFunctionFactory,AbstractBivariateStatWindowFunctionFactory.computeCorr, andAbstractBivariateStatWindowFunctionFactory.computeCorrWelford. Normal-magnitude inputs are unaffected and retain their prior bit-exact results.HTTP connections and contexts are reused across requests, but several parser and response objects were not fully resetting their per-request state, causing later requests to inherit stale state from earlier ones. In the worst case, a reused context could hit closed native parsing or compression buffers and crash the server. This fix reopens the header parser's quoted-value sink when pooled HTTP contexts are reused, clears per-request gzip negotiation state in
HttpResponseSink, clearscharsetand mapped cookie state inHttpHeaderParser.clear(), fixes multipart false-boundary replay so body bytes are preserved when a boundary-like sequence is followed by non-boundary suffixes, and parsesContent-Dispositionparameters with quote-aware delimiter handling. Uploaded filenames containing semicolons or equals signs (e.g.,a;b.csv,a=b;c.csv) now parse correctly.A query cancellation could be silently dropped when the cancel request arrived during the narrow window between query registration and circuit breaker flag binding.
NetworkSqlExecutionCircuitBreaker.cancel()always set thepowerUpTime == Long.MIN_VALUEsentinel but only flipped the per-query cancelled flag when it was already attached. If aCancelRequestlanded beforeQueryRegistry.register()bound the flag, the cancel survived only in the sentinel, which neithertestTimeout()nortestCancelled()consulted. The result was that the query ran to natural completion ignoring both cancellation and timeout, pinning workers. This fix makestestCancelled()check the sentinel first so both stateful paths abort a racing cancel, correctsgetState()to classify the sentinel asSTATE_CANCELLEDinstead ofSTATE_TIMEOUT, and clears the sentinel inPGConnectionContext.prepareForNewQuery()to bound it to a single query.Several per-partition code paths iterating posting columns were guarded only against absent columns (
columnTop == -1) but not row-less columns (columnTop >= partition size), causing two distinct failures. InrestorePostingIndexersToLastPartition, a row-less column with no.pkkey file would throwindex does not exist, causing WAL table suspension or direct commit failure. Conversely, blanket-skipping every row-less column was incorrect when one legitimately had a.pk(created byADD COLUMN ... INDEX TYPE POSTINGon the active partition), which would silently strand the indexer and make rows invisible to indexed predicates. This fix makesrestorePostingIndexersToLastPartitiondiscriminate by.pkfile existence rather than row-less-ness, and alignslinkPartitionIndexFiles(used during parquet partition switching) with its siblingcopyOrRebuildColumnIndexesby adding the fullcolumnTop == -1 || columnTop >= partitionSizeguard. The accessor was also changed fromgetColumnTopQuicktogetColumnTopto correctly return-1for absent columns.The
DataIDclass wraps a 128-bit UUID stored as two non-volatilelongfields (lo/hi). The value is published by one thread (the replication WAL downloader via a JNI up-call) and read by another, butinitialize()and the reader methods were not synchronized, whilechange()already was. This meant a reader could observe a torn half-written value or never observe the publish at all due to the lack of a happens-before edge. This fix synchronizes every accessor that touches thelo/hipair:initialize(),isInitialized(),getLo(),getHi(), andtoSink(). The fix is deliberately kept inDataIDrather than makingUuidfields volatile, sinceUuidis a hot general-purpose value holder used in tight loops, and the unsafe publication is specific toDataID's one-writer/other-reader pattern.This fix addresses a scenario where a transient
statfailure on a.pkfile during a column rename could cause permanent posting index corruption. The root cause was thatTableWriter.dropFuturePostingIndexChainEntriesBeforeLinksilently treated alength()error as "no chain," which caused the hard-link step to use a stale seal transaction number for the.pvfile. The only copy of the sealed values was then purged, leaving the partition with a.pkchain head referencing a nonexistent.pvfile. The fix throws aCairoException.criticalwhenlength()returns a negative value on an existing.pkfile, allowing the writer to distress and retry the operation. BothRENAME COLUMNandCONVERT PARTITIONcallers are affected. The legitimate short-file and absent-file paths retain their existing semantics.This fix resolves a crash (
AssertionError) that occurred when performing hash joins,GROUP BY, orDISTINCToperations on emptyVARCHARkeys marked as non-ASCII. An empty varchar is ASCII by definition, but theUtf8Sequencecontract allows producers to reportisAscii() == falsefor empty values. InUnorderedVarcharMap, an empty non-ASCII key packed to0, which collided with the map's empty-slot sentinel, causing silent table corruption. The fix addresses three layers:UnorderedVarcharMap.putVarcharnow forces the ASCII flag for empty keys; the QWP egress request decoder now computes the ASCII flag withUtf8s.isAscii()instead of hardcodingfalse; andVarcharTypeDriver.appendPlainValuenow forces the flag for empty values to avoid a zero-header collision with the NULL/empty sentinel. The column storage format is intentionally unchanged, as existing on-disk data remains valid.This fix addresses two related issues in index-backed
LATEST ONcursors. The first issue caused queries with an indexedSYMBOLkey and a residual filter (e.g.,WHERE sym = 'X' AND venue = 'Y' LATEST ON ts PARTITION BY sym) to return a row belonging to a different symbol value. The root cause was a double subtraction ofpartitionLofrom the row id —IndexReader.getCursor()already returns row ids relative tominValue, so the extra subtraction positioned the record too early in partitions spanning multiple page frames. The fix removes the redundant- partitionLoinLatestByValueIndexedFilteredRecordCursorandLatestByValuesIndexedFilteredRecordCursor. The second issue affected queries where the same key appeared both statically and as a runtime constant (e.g.,sym IN ('a', :sym)with:sym = 'a') — the duplicate inflatedkeyCountbeyond what thefoundset could reach, defeating the early-exit optimization and causing unnecessary full scans of older partitions. The fix ensures deferred keys that duplicate static keys are not double-counted, and reorders the frame loop condition to short-circuit before opening the next page frame once all keys are found.This fix resolves assertion failures that occurred when posting index covering scans (
symbol IN (...)) migrated across worker threads during suspendable queries such as multi-worker HTTP parquet exports. The assertion tripwire assumed a reader is driven by one OS thread between pool acquire and release, but suspendable queries legitimately migrate connections across workers with the event loop serializing the handoff. The fix stamps the reader's operating thread on eachgetCursor()call, and each cursorclose()path re-pools only when running on the stamping thread. Off-thread closes fall through toreleaseResources(), which frees only cursor-local buffers without touching reader-shared state. The fix also extends this gate to bitmap and all-null index readers for consistency, and corrects single-key covering scan parquet exports that produced all-null covered columns by addingRecordCursorFactory.producesMaterializedPageFrames()to route metadata-only producers through the row-wise cursor-based export path instead of the zero-copy page-frame path.Previously,
CREATE MATERIALIZED VIEWqueries that included a designated timestamp column but lacked aSAMPLE BYclause orGROUP BY timestamp_floor(...)column would incorrectly report "TIMESTAMP column is not present in select list," even when the timestamp was clearly in the select list. This fix distinguishes between two cases: when the timestamp column is present but no sampling interval can be inferred, the error now names the column and points toSAMPLE BYorGROUP BY timestamp_floor(...)as the two supported forms. When no designated timestamp column exists at all, the original generic message is preserved.CANCEL QUERY <id>could cancel a different, unrelated query after the registry recycled a pooled query entry, causing the victim query to fail with "cancelled by user" even though nobody cancelled it. This fix introduces a volatile lifecycle word per pooled entry that packs the owning query ID and state. Cancellation now only applies to the active query that owns the requested ID; a stale canceller holding a recycled registry entry fails the lifecycle check and reports the query as not found. Additionally,query_activity()now snapshots registry entries optimistically and only returns a row if the entry still belongs to the same active query after the row has been copied, preventing torn rows and wrong-query data. A query in theCANCELLINGstate can transiently not appear inquery_activity()while a canceller holds the lifecycle guard.dot_product()over twoDOUBLE[]/DOUBLE[][]arrays could return a silently incorrect result when one of the operands was not stored in vanilla (contiguous, row-major) layout. In the recursive code path, the deepest-dimension loop read the right-hand array with the left array's flat index rather than its own cursor. When both operands share an identical layout the two indices coincide, so the bug was masked. When the operands have different strides — e.g. one istranspose(...)or a slice — reading the right array with the left index selected the wrong element. This fix reads the right array with its own flat index. No behavior change occurs for arrays already stored in identical vanilla layout.Previously,
EXPLAINplan output over HTTP and CSV was HTML-encoded with for indentation and</>for comparison operators. This encoding was originally added as a presentation workaround for the Web Console but was applied to every HTTP request, making the response inconsistent with the PostgreSQL Wire Protocol output which returned plain text. This fix removes the HTML escaping path fromTextPlanSink, so all transports now return consistent plain-text plans with ordinary spaces for indentation and raw</>characters for operators. JSON serialization still performs its own required escaping. HTTPEXPLAINconsumers will now receive spaces instead of , and</>instead of</>.A WAL table with a POSTING index could be suspended mid-apply during a partition squash. Under parallel WAL apply,
PostingIndexWriter.ofreopened a column's.pkkey file and sized the mmap from the file length, which could lag the writer that just extended the file. This caused the chain's head entry to fall outside a short mapping, resulting in anAssertionErrorunder-eaor a SIGSEGV in a-dabuild. This fix addresses two issues: first, the reopen mapping is now sized to cover the header's live region (regionLimit) rather than the potentially stale file length, with range checks on the header before trusting the value; second, when a reopen fails before the region is republished, the.pkfile is no longer truncated back to the sentinel value, which previously could discard the live chain region on disk. On the dominant path where a cleanly-closed.pkalready covers the entry region, no extra remapping occurs.This fix replaces the previous peek-based connection probe with a hangup probe that detects a FIN behind buffered data on all platforms including Windows. The PostgreSQL Wire Protocol JDBC driver sends a Terminate message and WebSocket clients send a Close frame before the FIN, so the old
recv(MSG_PEEK)probe returned that buffered byte instead of EOF and the circuit breaker never tripped. The newNet.isPeerDisconnectedusespoll(POLLRDHUP)on Linux,kqueuewithEVFILT_READ/EV_EOFon macOS/FreeBSD, andWSAPollwithPOLLHUP|POLLERR|POLLNVALon Windows. The QuestDB WebSocket Protocol egress path now binds a per-connection, fd-backed circuit breaker instead of the no-op breaker, so egress queries observe bothquery.timeoutand the connection probe. Probe frequency is throttled by wall clock (default 100ms) so the fd-backed breaker does not turn hot loops into per-iteration syscalls. A stream resumed after a write park or credit grant re-checks the deadline and connection before producing the next batch. Detection is bound to the query-continuation wake interval rather than instantaneous, and a write-parked stream with a live but fully stalled reader is not aborted atquery.timeoutuntil it resumes. A double-read race inWriterPool.Entry.getTableToken()was also fixed where awriter_pool()scan racing a writer close could NPE.This fix addresses several correctness and resource-safety issues. The IPv4 not-null aggregators now implement the same keyed-batch semantics as other primitive types, so
first_not_null()andlast_not_null()return correct results for IPv4 columns on the non-sharded keyed-batch path. Thedim_length()function now returnsNULLforNULLarrays,NULLdimensions, andNULLPostgreSQL Wire Protocol array binds, instead of failing or returning incorrect values. Every parallel reducer now releases its worker slot from afinallyblock entered immediately after acquisition, preventing permanent slot leaks when frame navigation, temporary allocation, or cleanup fails. A data race on selectivity statistics shared across threads by thread-safe parallel filters was fixed by packing the sample count and average into a singlevolatile longupdated with a CAS loop. Array access overhead was reduced by reading dimension and element data directly through record accessors without materializing anArrayViewper row, and storage-delegating records forward both new methods including union, order-by materialization, window, join, and base columns underUNNEST.This fix resolves an issue where a table with a covering posting index, converted to Parquet and then back to native, returned wrong results for every covered column. Some covering scans returned
NULLfor covered columns while others failed with an index-out-of-bounds error. The root cause was thatconvertPartitionParquetToNativerebuilt the posting index viarebuildPartitionIndexFiles, which builds only the non-covering.pvfiles and never the covering sidecars (.pci/.pc*). A second cause was found in the native-to-Parquet direction: when a column had a non-zero column top,copyOrRebuildColumnIndexesrebuilt the index without covering configuration, dropping covered values. The fix hard-links existing index files from the Parquet partition directory into the new native directory instead of rebuilding, since row IDs are preserved and covering sidecars are storage-independent value copies. Both conversion directions now share onerebuildColumnIndexmethod that callsconfigureCoveringIfNeededand correctly points the sidecar build at the directory holding the native column data. This also routes plain bitmap symbol indexes through the link path on reconvert, reducing index work from a full-column scan to a few syscalls.On x86 systems with SQL JIT enabled, a valid query checking a
STRINGcolumn forNULLcould rarely crash QuestDB with SIGSEGV. This occurred when a null or empty string header occupied the final four bytes of a mapped column file, causing the scalar JIT to read beyond the mapping boundary. This fix ensures the four-byteSTRINGheader is read without crossing the mapping boundary. As a workaround for earlier versions, settingcairo.sql.jit.mode=offand restarting QuestDB avoids the crash.This fix ensures the table details drawer correctly understands the current storage-policy grammar (
TO PARQUET/TO REMOTE/DROP LOCAL/DROP REMOTE). Previously a policy containingTO REMOTEfailed to parse and the drawer showed "Not configured" for tables that had one. Additionally, creating a materialized view on a policy-bearing source now generates DDL the server accepts: the policy'sDROP LOCALstage becomes a laddered materialized viewTTL. Servers rejectSTORAGE POLICYon materialized views, so the previously generated DDL always failed. The@questdb/sql-parserdependency was bumped to 0.1.15, which addsTO REMOTEsupport, removes the unsupportedDROP NATIVE, and registersstorage_policies()for autocomplete.This fix removes HTML entity decoding from the modern and legacy result grids, preserving result values exactly as returned by the server in rendered cells, clipboard output, and Markdown export. Previously, the Web Console applied
unescapeHtmlto every result value, which made ordinary query results lossy: legitimate values such as ,<,>,&,", and'were incorrectly transformed even though they were user data. With a paired QuestDB server change that returnsEXPLAINplans as plain text, the Web Console now treats every response value as data. Leading whitespace is also preserved so plain-textEXPLAINplans keep their hierarchy viawhite-space: pre.This fix resolves several issues with query execution in the Web Console. Previously, with all SQL selected, clicking the Cancel button on a running query opened the "Run all queries" modal instead of cancelling, because Run and Cancel shared a handler that branched on the selection. Now Cancel always cancels while a query is running, regardless of selection state. The selection is carried into the pending run so the confirmation dialog correctly reflects "Run selected queries." Additionally, triggering a multi-statement selection run while a query was already running silently ran all queries instead of the selected ones. The new-tab button, which appeared greyed out during a script run but was still clickable due to a CSS-only guard, is now gated by logic for both the button and Alt+T shortcut.