QuestDB Enterprise 3.3.1 is a maintenance release on the same QuestDB 9.4.x engine as 3.3.0. It hardens the storage-policy and posting-index features introduced in 3.3.0 — including a fix for a JVM crash in the posting index — pulls in a batch of SQL correctness and crash fixes from the OSS engine, and adds a custom root CA option for the replication object store and an EXCLUDE column list for column-level GRANT/REVOKE.
New Features
Column-level
GRANT/REVOKEnow supports anEXCLUDEclause to target all columns except a specified set without listing every column explicitly. For example:GRANT SELECT ON foo(* EXCLUDE(a, b, c)) TO my_user;. The*means all columns of the table, optionally followed byEXCLUDE(...)to omit some. At statement time, the table's current columns are enumerated, excluded ones are dropped, and the permission is granted or revoked on each remaining column individually. Columns added after the statement runs are not covered automatically. Additionally,REVOKEis now strict: aREVOKEthat names a table or column which does not exist will error instead of silently succeeding, catching typos that would otherwise leave the operator believing access was revoked.GRANTremains lenient about missing tables since Influx Line Protocol can auto-create tables on first ingestion. Every column named inEXCLUDE(...)must exist on the table, and duplicates or typos produce errors. Errors are positioned at the offending token for clear diagnostics.This feature adds two optional parameters to the object store configuration string for HTTP-based services (
s3,azblob,gcs):ca_cert_file=/path/to/ca.pemtrusts the CA root certificate(s) in the specified PEM file in addition to the built-in Mozilla/webpki roots, andca_builtin_roots=falsedrops the built-in roots to trust only the certificates fromca_cert_file. For example:s3::region=us-east-1;root=/bucket/path;endpoint=https://minio.internal;ca_cert_file=/etc/ssl/private-ca.pem;. Both keys are rejected for thefsservice. The PEM is read and parsed during configuration validation, so a missing or malformed file fails fast. This is needed because the replication object store client uses rustls with the bundled webpki-roots only, ignoring the OS trust store,SSL_CERT_FILE/SSL_CERT_DIR, andAWS_CA_BUNDLE. Private or on-prem S3/Azure/GCS-compatible stores fronted by a private CA, self-signed certificate, or TLS-intercepting proxy previously failed the TLS handshake with no way to add the trusted CA. Parsing of the object store configuration string is now also stricter: a;-separated parameter with no=is rejected with an error instead of being silently dropped.
Bug Fixes
When a partial upload advance encountered the first pending transaction whose WAL segment was still open, it consumed zero sequencer entries and incorrectly reported an error identical to a genuine catastrophic condition (missing part files). This caused the table to drop to the slow retry batch size and sleep the retry interval, even though deferring transactions in open segments is the designed behavior for partial mode. On production systems with many long-lived open WAL segments (e.g., many low-rate Influx Line Protocol writers), this resulted in hundreds of spurious errors per hour, each triggering a slow-mode flip and retry sleep, causing upload throughput to fall behind ingestion rate. This fix ensures the uploader only reports an error when zero entries were read without curtailment (the genuine unreadable-txnlog case). Zero-entry curtailment now takes the existing graceful path with no error, no slow-mode flip, and no retry sleep, and the caller resets to the fast batch size as for any successful advance.
QuestDB Enterprise 3.3.0 beta builds created
sys.sp_entrieswith adrop_nativecolumn, but the current schema replaced it withto_remote. These are not equivalent, and sinceCREATE TABLE IF NOT EXISTScannot repair an existing beta table, the writer would write into a mislabelled column and the reader would fail to compile itsto_remotequery. A new system migration (SysMig5) now runs on a primary before the storage-policy reader initializes. When it detects the beta schema, it drops thestorage_policiessystem view so it can be recreated with the current definition, renamessys.sp_entriesandsys.sp_linksto*_v1_backupto preserve the rows, and fails startup with an actionable message. The next start creates the current schema. The migration is also safe under the replica-first upgrade procedure:StoragePolicyCheckJobcatches compilation failures on the replica and retries until the primary is upgraded and the schema changes replicate.StoragePolicyReadernow detects when the table ID behindsp_entries/sp_linkschanges, drops cached trackers, and reloads so the replica self-heals without an extra restart.This fix addresses a set of correctness, resource, and concurrency bugs in the posting/covering index that surfaced across partition squash, the plain O3 commit path, parquet partition reseal, and the WAL fast-lag apply path. The primary issue was a JVM crash (
SIGSEGV) or missing rows after partition squash on tables with aPOSTINGindex. WhencommitDense()rebuilt a large or squashed partition that exceeded the spill budget (cairo.posting.index.indexer.spill.bytes.max), mid-stream compaction persisted sparse generations to the.pvfile, but the subsequent dense generation write orphaned them — causing crashes for covering indexes or silently dropped rows for non-covering indexes. The fix consolidates throughseal()when generations already exist, re-encoding every generation into a single dense gen-0 at offset 0. Additional fixes include: parquet partitions with covering posting indexes now have their.pci/.pcsidecars rebuilt during O3 reseal instead of returning NULL for covered columns; the WAL fast-lag apply path now restores covering configuration before indexing to prevent incomplete sidecars after mid-stream spills;ALTER TABLE ... ALTER COLUMN ... SYMBOL CAPACITYnow preserves covering-column indices; a leaked.pvfile during parquet reseal spills is now properly cleaned up via deferred seal-purges; and all seal-purge state access is now protected by a single lock for thread safety. A bareINDEX TYPE POSTINGwith noINCLUDEclause is now non-covering by default — the previous implicit covering behavior was unintended. Existing tables retain their persisted covering flag.A
COUNT()over a keyedGROUP BYsubquery filtered on an aggregate alias (a HAVING-style predicate) reported duplicate groups that did not exist. The root cause was in the query optimizer's column propagation:propagateTopDownColumns0()had a guard that re-added grouping keys to top-down columns to prevent column pruning, but it ran before the model's ownWHERE/HAVINGliterals had been emitted. For theCOUNT()shape where the outer query contributes no key literals, the keys were left unprotected, pruning collapsed the keyedGROUP BYinto a scalar aggregate, and the result was incorrect. The fix ensuresretainGroupByKeysAsTopDownColumns()runs twice — once at the original early position and once afterWHERE/HAVINGandORDER BYliterals have been emitted. The second pass is idempotent via alias deduplication and only adds keys where the early pass missed them.An aggregate (
COUNT(),SUM(), etc.) over aUNION ALLof aliased sub-queries crashed query compilation with anAssertionError, surfacing as a 500 in the Web Console. The root cause was that the optimizer's column-propagation machinery resolved literals acrossUNIONboundaries by name rather than by position. When the outer query selected nothing from the union (an aggregate), this pruned one branch down to a few matching-by-name columns while leaving other branches intact, causing the branches to disagree on column count. SinceUNIONcolumns are matched by position, not name, the fix removes the superseded by-name emit in favor of the indexed, by-position propagation. A rollback flagcairo.sql.legacy.union.column.propagation(defaultfalse) is available to restore the old behavior if needed.SHOW CREATE TABLEpreviously resolved any object kind and rendered aCREATE TABLEstatement for it. When run against a view or materialized view, it produced misleading DDL that looked like a table definition and would have created a plain table rather than the view if executed. This fix makesSHOW CREATE TABLEthrow an error when the resolved token is a view or materialized view, reporting "table name expected, got view or materialized view name." The dedicatedSHOW CREATE VIEWandSHOW CREATE MATERIALIZED VIEWstatements should be used instead. Additionally, the sibling guards were tightened for symmetry so thatSHOW CREATE VIEW <matview>now reports "got materialized view name" andSHOW CREATE MATERIALIZED VIEW <view>reports "got view name" rather than the generic "got table name."This fix addresses numerous engine bugs surfaced by expanded query fuzzer coverage. Key fixes include: an off-by-one error when stepping past null array map keys in
OrderedMapVarSizeRecord; ASOF join light path crashes for STRING/VARCHAR-to-SYMBOL joins;ClassCastExceptionin parallel keyedGROUP BYover covering index factories; memory leaks from row cursor factories not being freed on exceptions ortoTop()calls; incorrect scan direction advertisement and duplicate-key handling for multi-key covering index queries; per-worker array key memory leaks inGROUP BY; incorrect group-by alias resolution for outer columns on duplicate references; unsafeLIMITpush through trivial group-by expressions whenORDER BYcolumns were absent from theGROUP BY; posting-indexDISTINCToutput named by source token instead of projection alias; JIT narrow-operand widening failures whenLONGappeared only as a literal; full-fat ASOF/LT join projection crashes with cross-typeSYMBOLkeys;SymbolConstant.valueOf()returning non-null forVALUE_IS_NULLkeys; qualified column resolution failures underDISTINCT; and a missingINfunction factory for IPv4 columns. The newInIPv4FunctionFactoryaccepts NULL, STRING, VARCHAR, SYMBOL, IPv4, and bind variables, usingLongHashSetstorage to safely cover the full 32-bit IPv4 range.This fix addresses five production bugs exposed by stronger assertion coverage. Cross join
skipRows()was not re-entrant: a second call while the cursor sat partway through a master row's slave scan re-skipped the already-consumed master cursor and dropped remaining slave rows, causingLIMITand result-size calculation over cross joins to count or skip too few rows. Multi-value indexed latest-by (<indexed symbol> IN (...) LATEST ON ts) emitted result rows in index/partition discovery order without sorting across multiple partitions, yet advertised a forward scan direction, so the optimizer elidedORDER BY tsand returned unsorted rows. Parquet random access could read freed memory and crash the JVM:PageFrameMemoryPool.navigateTo()early-returned when the record's frame index matched the requested one, but anAsyncFilteredRecordCursorrecord could be bound to a reduce task's parquet buffers that were freed eagerly on collect, causing subsequent column reads to dereference freed native memory. Dense ASOF join produced stale results when its cursor was re-read becausetoTop()did not resetbackwardScanExhausted, causing matches to be dropped on every pass after the first. Finally,COUNT(*)over a full scan threw on an empty partition missing from disk becausecalculateSize()opened every partition unconditionally, unlikenext()which already skips zero-row partitions.