feat(sql): add rowCount, txn and timestamp columns to tables()#6581
feat(sql): add rowCount, txn and timestamp columns to tables()#6581bluestreak01 merged 34 commits intomasterfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughIntroduces RecentWriteTracker, a concurrent metrics system for tracking recently written tables with per-table statistics including timestamps, row counts, WAL metrics, merge stats, and replica tracking. Integrates tracker throughout the system: CairoEngine, writer pools, WalWriter, and ServerMain. Significantly expands the tables() function schema with 30+ new columns. Extends Function-related interfaces with new accessors and lifecycle hooks. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
core/src/main/java/io/questdb/cutlass/line/array/AbstractArray.java (1)
78-109: Initialize memA after applyShape() to match reshape pattern.The constructor calls
array.applyShape()at line 107 but doesn't reinitializememAafterward. All reshape methods (lines 211, 236, 263, 293) consistently callmemA = array.startMemoryA()afterapplyShape(), indicating thatapplyShape()changes array state in a way that requires refreshing the memory appender.Since
memAis initialized at field declaration (line 76) before the array is properly configured, it may be stale after the constructor completes.🔎 Proposed fix
array.applyShape(); flatLength = array.getFlatViewLength(); + memA = array.startMemoryA(); }core/src/main/java/io/questdb/cutlass/http/client/AbstractChunkedResponse.java (1)
68-83: Add parameter validation and improve documentation.The method accepts raw memory addresses without validation. While current callers in HttpClient manage buffers correctly, the public API lacks defensive checks:
- Missing bounds validation: No verification that
loandhifall within[bufLo, bufHi]- Missing ordering validation: No check that
lo <= hi- Insufficient documentation: JavaDoc omits preconditions (valid address ranges, when safe to call) and thread-safety guarantees
A caller could pass reversed or out-of-bounds addresses, causing memory corruption in downstream operations.
🔎 Suggested validation improvements
/** * Begins processing a new chunk of response data. + * This method resets the internal state and should only be called + * before starting a new response or after completing the previous one. + * Not thread-safe: concurrent calls or calls during active processing + * will corrupt internal state. * - * @param lo the low address of the data - * @param hi the high address of the data + * @param lo the low address of the data (must be >= bufLo and <= bufHi) + * @param hi the high address of the data (must be >= lo and <= bufHi) + * @throws IllegalArgumentException if parameters are out of valid range */ public void begin(long lo, long hi) { + if (lo < bufLo || lo > bufHi) { + throw new IllegalArgumentException("lo out of buffer range [bufLo=" + bufLo + ", bufHi=" + bufHi + ", lo=" + lo + "]"); + } + if (hi < lo || hi > bufHi) { + throw new IllegalArgumentException("hi out of buffer range or less than lo [bufLo=" + bufLo + ", bufHi=" + bufHi + ", lo=" + lo + ", hi=" + hi + "]"); + } this.dataLo = lo; this.dataHi = hi; this.state = STATE_CHUNK_SIZE; this.receive = hi == lo; this.endOfChunk = false; size = 0; available = 0; consumed = 0; }
🧹 Nitpick comments (24)
core/src/main/java/io/questdb/std/datetime/AbstractDateFormat.java (1)
30-32: Good: Javadoc added to abstract class.The documentation is clear and syntactically correct. Consider expanding it with implementation guidance or behavioral notes for subclasses if needed—e.g., details on parse semantics or thread-safety expectations.
core/src/main/java/io/questdb/cairo/AbstractFullPartitionFrameCursor.java (1)
34-128: Documentation additions look good.The Javadoc comments added throughout this file are accurate and follow standard conventions. While they are somewhat terse (mostly restating field/method names), they provide a baseline level of documentation that improves code readability.
Optional: Consider enriching documentation for better maintainability
If you'd like to enhance these docs further, consider adding:
- Context and purpose: Explain what partition frames represent and how they're used in the cursor lifecycle
- Invariants: Document relationships between fields (e.g., partitionIndex range relative to partitionHi)
- Thread safety: Note any concurrency expectations for readers using this cursor
- Lifecycle: Clarify when fields are initialized and their validity scope
Example for
partitionHi:/** - * The partition high boundary. + * The partition high boundary (exclusive upper bound). + * Represents the total number of partitions in the reader. + * Valid after of(TableReader) is called; updated on reload(). */ protected int partitionHi;This is purely optional and can be deferred to future documentation improvements.
core/src/main/java/io/questdb/griffin/engine/functions/groupby/AbstractCountGroupByFunction.java (1)
38-48: Documentation added for class and fields.The JavaDoc additions follow standard conventions and improve code maintainability. The documentation is functional but could be more descriptive to provide additional context (e.g., explaining what kind of function
argrepresents in count operations, or which mapvalueIndexrefers to).core/src/main/java/io/questdb/cutlass/http/client/Fragment.java (1)
27-43: Consider enhancing the documentation with additional context.The added Javadoc is clear but minimal. For better developer experience, consider documenting:
- What the memory addresses represent (raw pointers, buffer offsets, etc.)
- Thread-safety guarantees
- Lifecycle and validity (when is the Fragment valid? who owns the memory?)
- Relationship to Response (since Fragment is returned by Response.recv())
📝 Example enhanced documentation
/** - * Represents a fragment of data with low and high memory addresses. + * Represents a contiguous fragment of HTTP response data in native memory. + * The fragment is valid only until the next call to Response.recv() or until + * the Response is closed. The memory addresses are raw pointers to native memory + * managed by the HTTP client. + * <p> + * This interface is not thread-safe and should only be accessed from the thread + * that received it from Response.recv(). */ public interface Fragment { /** - * Returns the high address of this fragment. + * Returns the exclusive upper bound address of this fragment in native memory. * - * @return the high address + * @return the high address (exclusive) */ long hi(); /** - * Returns the low address of this fragment. + * Returns the inclusive lower bound address of this fragment in native memory. * - * @return the low address + * @return the low address (inclusive) */ long lo(); }core/src/main/java/io/questdb/cutlass/http/client/Response.java (1)
31-44: Consider enhancing the method documentation.The documentation for both
recv()methods could be more comprehensive. Consider adding:
- What the "default timeout" value is for
recv()- What happens when a timeout occurs (exception thrown? null returned? special Fragment value?)
- Whether
timeoutvalues of 0 or negative numbers have special meaning- Thread-safety guarantees
- Whether Fragment can be null on error conditions
📝 Example enhanced documentation
/** - * Receives the next fragment of response data using the default timeout. + * Receives the next fragment of response data using the default timeout (5000ms). + * Blocks until data is available or the timeout expires. * - * @return the received fragment + * @return the received fragment, never null + * @throws io.questdb.cairo.HttpException if the timeout expires or connection fails */ Fragment recv(); /** - * Receives the next fragment of response data with the specified timeout. + * Receives the next fragment of response data with the specified timeout. + * Blocks until data is available or the timeout expires. * - * @param timeout the timeout in milliseconds - * @return the received fragment + * @param timeout the timeout in milliseconds (0 = no timeout, negative = default) + * @return the received fragment, never null + * @throws io.questdb.cairo.HttpException if the timeout expires or connection fails + * @throws IllegalArgumentException if timeout is negative and not a special value */ Fragment recv(int timeout);core/src/main/java/io/questdb/griffin/engine/groupby/vect/AbstractCountVectorAggregateFunction.java (1)
57-61: Consider using private final fields or adding null checks for protected fields used by subclasses.The fields
distinctFuncandkeyValueFuncare declared in this abstract class but never referenced in its methods. Subclasses likeCountIntVectorAggregateFunctionassign these fields in their constructors and then use them directly inaggregate()without null checks. While the current Count* subclasses properly initialize these fields in all code paths, this pattern is error-prone: if a subclass forgets to initialize either field, an NPE will occur at runtime with no way to detect it in the abstract class. Other similar aggregate functions in this package useprivate finalfields instead, which is more explicit and prevents accidental misuse by subclasses.core/src/main/java/io/questdb/std/AbstractCharSequenceHashSet.java (1)
120-128: Consider defensive validation in keyAt.The method assumes
indexis negative (as documented), but passing a non-negative index would cause incorrect array access. Adding a precondition check would improve safety:public CharSequence keyAt(int index) { assert index < 0 : "index must be negative"; return keys[-index - 1]; }However, this is a performance tradeoff since the negative-index convention is already documented.
core/src/test/java/io/questdb/test/ServerMainTest.java (1)
127-202: Well-structured hydration test with minor robustness suggestion.The test correctly validates
RecentWriteTrackerhydration across server restarts. The setup withwait_wal_table()ensures deterministic state, and setting the hydration callback beforestart()is the correct order.One minor robustness concern:
hydrationLatch.await()on line 181 has no timeout, which could cause the test to hang indefinitely if hydration fails unexpectedly. Consider adding a timeout variant ifSOCountDownLatchsupports it.🔎 Optional: Add timeout to prevent test hanging
If
SOCountDownLatchsupports a timed await, consider:// Wait for hydration with timeout to prevent indefinite hang boolean completed = hydrationLatch.await(30, TimeUnit.SECONDS); Assert.assertTrue("Hydration should complete within timeout", completed);core/src/main/java/io/questdb/cairo/wal/ApplyWal2TableJob.java (2)
522-522: Simplify redundant double rounding.Line 522 applies
Numbers.roundUptwice to the same value, which is redundant. The expressionNumbers.roundUp(Numbers.roundUp(100.0 * physicalRowsAdded / rowsAdded, 2) / 100.0, 2)can be simplified.🔎 Proposed simplification
- double amplification = rowsAdded > 0 ? Numbers.roundUp(Numbers.roundUp(100.0 * physicalRowsAdded / rowsAdded, 2) / 100.0, 2) : 0; + double amplification = rowsAdded > 0 ? Numbers.roundUp((double) physicalRowsAdded / rowsAdded, 2) : 0;
660-662: Consider wrapping tracker call in try-catch for consistency.The
recordWalProcessedcall is not wrapped in a try-catch block, unlike the similarrecordWritecall in WriterPool.java (lines 577-582). If tracking fails here, it could disrupt WAL processing.🔎 Proposed defensive wrapping
- // Decrement pending WAL row count and track dedup after successful processing - engine.getRecentWriteTracker().recordWalProcessed(writer.getTableToken(), lastCommittedSeqTxn, lastCommittedRows, rowsCommitted); - + // Decrement pending WAL row count and track dedup after successful processing + try { + engine.getRecentWriteTracker().recordWalProcessed(writer.getTableToken(), lastCommittedSeqTxn, lastCommittedRows, rowsCommitted); + } catch (Throwable th) { + LOG.error().$("failed to track WAL processing [table=").$(writer.getTableToken()) + .$(", error=").$(th).I$(); + } +core/src/main/java/io/questdb/PropServerConfiguration.java (1)
391-391: RecentWriteTracker capacity wiring looks consistent; optional validationThe new
recentWriteTrackerCapacityfield is read fromCAIRO_RECENT_WRITE_TRACKER_CAPACITYwith a default of 1000 and exposed viaPropCairoConfiguration.getRecentWriteTrackerCapacity(), which aligns with the tracker’s configuration needs.If
RecentWriteTrackerdoes not explicitly handle non‑positive capacities, consider validating (e.g.,> 0) here and throwing aServerConfigurationExceptionon invalid values to fail fast with a clearer message rather than deferring to deeper runtime failures.Also applies to: 1416-1417, 3658-3661
core/src/main/java/io/questdb/griffin/engine/functions/BinaryFunction.java (1)
35-157: BinaryFunction default behavior is solid; be mindful of child contracts and commutativityCentralizing binary-function lifecycle and state (init/close/memoize/toPlan/etc.) here is a good cleanup and should significantly reduce duplication across binary function implementations.
Two soft-contract points to keep in mind for implementors:
getLeft()/getRight()are implicitly assumed non‑null and independently closeable; if any implementation ever reuses the sameFunctioninstance for both sides, that child will seeclose()/cursorClosed()twice. Either avoid such sharing or ensure those children are idempotent.isEquivalentTois structurally order-sensitive; for truly commutative operators that should treata op bequivalent tob op a, consider overridingisEquivalentToin those specific implementations.Otherwise, the defaults (especially for constantness, runtime-constant detection, and parallelism flags) look consistent with existing Function semantics.
core/src/main/java/io/questdb/griffin/engine/functions/eq/AbstractEqBinaryFunction.java (1)
32-73: AbstractEqBinaryFunction cleanly integrates equality functions with BinaryFunctionThe new base class correctly encapsulates
left/rightoperands, exposes them viagetLeft()/getRight(), and provides a negation-awaretoPlanthat yieldsa=bora!=bas expected. This should simplify equality-function implementations and make them benefit fromBinaryFunction’s shared lifecycle and state-handling defaults without extra boilerplate.If you later want to rely on
BinaryFunction’s generic operator rendering, you could overrideisOperator()to returntrueand delegate to the defaulttoPlan, but the dedicatedtoPlanhere is perfectly fine and explicit.core/src/test/java/io/questdb/test/cairo/pool/RecentWriteTrackerTest.java (1)
207-227: Eviction test assertion message vs bound is slightly inconsistentIn
testEviction, the comment and failure message say “size should be around capacity (5)” / “<= capacity after eviction”, but the assertion usestracker.size() <= 10, effectively allowing up to2 * capacityto match the current implementation’s eviction threshold.To avoid confusion (and future maintenance mistakes if the eviction policy changes), consider:
- Updating the comment and assertion message to explicitly say “<= 2x capacity”, or
- Deriving the bound from the configured capacity (e.g.,
2 * capacity) instead of the literal10.benchmarks/src/main/java/org/questdb/RecentWriteTrackerBenchmark.java (1)
207-221: Consider adding a note clarifying the simplifiedWriteStatsdesign.The benchmark's
WriteStatsis intentionally simplified compared to the productionRecentWriteTracker.WriteStats(which usesAtomicLong,LongAdder, histograms, and locks). Since the benchmark targets lambda allocation patterns rather than full stat tracking semantics, this is appropriate, but a brief comment would help future readers understand this design choice.Suggested documentation
- // Simple WriteStats class for benchmarking + // Simplified WriteStats class for benchmarking allocation patterns. + // The production RecentWriteTracker.WriteStats uses atomic fields, histograms, + // and locks - omitted here since we're only measuring lambda capture overhead. public static class WriteStats {core/src/test/java/io/questdb/test/griffin/engine/functions/catalogue/TablesFunctionFactoryTest.java (2)
218-226: Multiline string literal formatting could be clearer.The embedded
writerTxnvalue concatenation within the multiline string creates unusual formatting. Consider usingString.formator a simpler concatenation approach for better readability.🔎 Suggested improvement
- assertSql( - """ - table_name\twriterTxn\tsequencerTxn\tlastWalTimestamp - test_non_wal\t""" + writerTxn + """ - \tnull\t - """, - "select table_name, writerTxn, sequencerTxn, lastWalTimestamp from tables() where table_name = 'test_non_wal'" - ); + assertSql( + "table_name\twriterTxn\tsequencerTxn\tlastWalTimestamp\n" + + "test_non_wal\t" + writerTxn + "\tnull\t\n", + "select table_name, writerTxn, sequencerTxn, lastWalTimestamp from tables() where table_name = 'test_non_wal'" + );
374-382: Similar multiline string formatting issue.Same readability concern with the embedded values in multiline string literals.
core/src/test/java/io/questdb/test/cairo/pool/RecentWriteTrackerIntegrationTest.java (2)
48-76: Test lacks meaningful assertions.This test serves as documentation but only asserts that the tracker is not null. Consider either adding substantive assertions or converting this to actual documentation/Javadoc.
320-321:Thread.sleep(1)is unreliable for timestamp differentiation.Sub-millisecond sleeps are not guaranteed by the JVM and may not produce different timestamps on fast systems. The test assertion uses
>=which handles this, but the comment suggests the intent is to ensure different timestamps.🔎 Alternative approach
Consider using a test clock or explicitly advancing time instead of relying on
Thread.sleep(1):// Small delay to ensure different timestamp // Note: Using >= assertion since Thread.sleep(1) may not guarantee different microsecond timestampsOr use a more robust approach that doesn't depend on timing at all.
core/src/main/java/io/questdb/cairo/pool/RecentWriteTracker.java (2)
534-542: Recursive quicksort may cause StackOverflowError for large datasets.With default capacity of 1000 and eviction at 2x (2000 entries), a worst-case sorted input could require ~2000 stack frames. While typical JVM stacks can handle this, consider using an iterative approach or Java's built-in sorting for robustness.
🔎 Alternative using iterative or built-in sort
For robustness, consider using an iterative quicksort with an explicit stack, or sort using indices into an array:
// Option: Use indices array and Arrays.sort with comparator int[] indices = new int[size]; for (int i = 0; i < size; i++) indices[i] = i; // Sort indices by timestamps descending
494-508: O(n*k) eviction algorithm may cause latency spikes.When evicting from 2x capacity down to capacity, this iterates the entire map
ktimes (wherek = capacity). For the default capacity of 1000, this means 1000 full iterations over 2000 entries = 2M operations. Consider using a min-heap or sorting once.🔎 More efficient eviction approach
// Instead of k iterations finding minimum each time: // 1. Collect all entries with timestamps // 2. Sort once // 3. Remove oldest k entries List<Map.Entry<TableToken, WriteStats>> entries = new ArrayList<>(writeStats.entrySet()); entries.sort(Comparator.comparingLong(e -> e.getValue().getMaxTimestamp())); for (int i = 0; i < toEvict && i < entries.size(); i++) { writeStats.remove(entries.get(i).getKey()); }core/src/main/java/io/questdb/griffin/engine/functions/catalogue/TablesFunctionFactory.java (3)
266-282: Assert statement may cause unexpected failures.Line 280 uses
assert col == MAX_UNCOMMITTED_ROWS_COLUMNafter severalifstatements. If a new INT column is added but not handled, this will fail with assertions enabled. Consider using an explicitelse ifwith a default case.🔎 Safer pattern
@Override public int getInt(int col) { if (col == ID_COLUMN) { return table.getId(); } if (col == TTL_VALUE_COLUMN) { return getTtlValue(table.getTtlHoursOrMonths()); } if (col == MEMORY_PRESSURE_LEVEL_COLUMN) { if (!table.isWalEnabled()) { return Numbers.INT_NULL; } SeqTxnTracker tracker = tableSequencerAPI.getTxnTracker(table.getTableToken()); return tracker.getMemPressureControl().getMemoryPressureLevel(); } - assert col == MAX_UNCOMMITTED_ROWS_COLUMN; - return table.getMaxUncommittedRows(); + if (col == MAX_UNCOMMITTED_ROWS_COLUMN) { + return table.getMaxUncommittedRows(); + } + return Numbers.INT_NULL; }
284-296: Inconsistent return value for null writeStats.When
writeStats == null, the method returns0.0, but the default case returnsDouble.NaN. This inconsistency could confuse consumers. Consider returningDouble.NaNconsistently for missing data.🔎 Consistent null handling
@Override public double getDouble(int col) { if (writeStats == null) { - return 0.0; + return Double.NaN; } return switch (col) {
303-313: Complex null-handling condition is hard to maintain.The long chain of
||conditions for determining which columns return0vsNumbers.LONG_NULLis error-prone when adding new columns. Consider grouping related columns or using a Set for cleaner logic.🔎 Alternative approach using column groups
// Define sets of columns that return 0 when writeStats is null private static final IntHashSet ZERO_WHEN_NULL_COLUMNS = new IntHashSet(); static { ZERO_WHEN_NULL_COLUMNS.add(PENDING_ROW_COUNT_COLUMN); ZERO_WHEN_NULL_COLUMNS.add(DEDUPE_ROW_COUNT_COLUMN); ZERO_WHEN_NULL_COLUMNS.add(TXN_COUNT_COLUMN); // ... etc } // Then in getLong: if (writeStats == null) { return ZERO_WHEN_NULL_COLUMNS.contains(col) ? 0 : Numbers.LONG_NULL; }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (97)
benchmarks/src/main/java/org/questdb/RecentWriteTrackerBenchmark.javacore/src/main/java/io/questdb/DefaultBootstrapConfiguration.javacore/src/main/java/io/questdb/PropServerConfiguration.javacore/src/main/java/io/questdb/PropertyKey.javacore/src/main/java/io/questdb/ServerMain.javacore/src/main/java/io/questdb/cairo/AbstractFullPartitionFrameCursor.javacore/src/main/java/io/questdb/cairo/AbstractRecordCursorFactory.javacore/src/main/java/io/questdb/cairo/CairoConfiguration.javacore/src/main/java/io/questdb/cairo/CairoConfigurationWrapper.javacore/src/main/java/io/questdb/cairo/CairoEngine.javacore/src/main/java/io/questdb/cairo/DefaultCairoConfiguration.javacore/src/main/java/io/questdb/cairo/TableWriter.javacore/src/main/java/io/questdb/cairo/mv/WalTxnRangeLoader.javacore/src/main/java/io/questdb/cairo/pool/RecentWriteTracker.javacore/src/main/java/io/questdb/cairo/pool/WalWriterPool.javacore/src/main/java/io/questdb/cairo/pool/WriterPool.javacore/src/main/java/io/questdb/cairo/sql/Function.javacore/src/main/java/io/questdb/cairo/sql/FunctionExtension.javacore/src/main/java/io/questdb/cairo/sql/PartitionFrameCursor.javacore/src/main/java/io/questdb/cairo/sql/RecordCursorFactory.javacore/src/main/java/io/questdb/cairo/sql/SymbolTableSource.javacore/src/main/java/io/questdb/cairo/wal/ApplyWal2TableJob.javacore/src/main/java/io/questdb/cairo/wal/CheckWalTransactionsJob.javacore/src/main/java/io/questdb/cairo/wal/WalMetrics.javacore/src/main/java/io/questdb/cairo/wal/WalReader.javacore/src/main/java/io/questdb/cairo/wal/WalWriter.javacore/src/main/java/io/questdb/cairo/wal/seq/SeqTxnTracker.javacore/src/main/java/io/questdb/client/ArraySender.javacore/src/main/java/io/questdb/cutlass/http/client/AbstractChunkedResponse.javacore/src/main/java/io/questdb/cutlass/http/client/Fragment.javacore/src/main/java/io/questdb/cutlass/http/client/Response.javacore/src/main/java/io/questdb/cutlass/line/array/AbstractArray.javacore/src/main/java/io/questdb/griffin/FunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/BinaryFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/GroupByFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/SymbolFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/UnaryFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToBooleanFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToByteFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToCharFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToDateFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToDecimal64Function.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToDecimalFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToDoubleFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToFloatFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToGeoHashFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToIPv4Function.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToIntFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToLong256Function.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToLongFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToShortFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToStrFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToSymbolFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToTimestampFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToUuidFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/cast/AbstractCastToVarcharFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/catalogue/AbstractEmptyCatalogueFunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/catalogue/TablesFunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/date/AbstractDayIntervalFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/date/AbstractDayIntervalWithTimezoneFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/date/AbstractGenerateSeriesRecordCursorFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/decimal/ToDecimal64Function.javacore/src/main/java/io/questdb/griffin/engine/functions/decimal/ToDecimalFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/eq/AbstractEqBinaryFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/groupby/AbstractCountDistinctIntGroupByFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/groupby/AbstractCountGroupByFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/groupby/AbstractCovarGroupByFunction.javacore/src/main/java/io/questdb/griffin/engine/functions/math/AbsDecimalFunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/math/AbsDoubleFunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/math/AbsIntFunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/math/AbsLongFunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/math/AbsShortFunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/groupby/vect/AbstractCountVectorAggregateFunction.javacore/src/main/java/io/questdb/griffin/engine/groupby/vect/VectorAggregateFunction.javacore/src/main/java/io/questdb/griffin/engine/join/AbstractAsOfJoinFastRecordCursor.javacore/src/main/java/io/questdb/griffin/engine/table/AbstractDeferredTreeSetRecordCursorFactory.javacore/src/main/java/io/questdb/griffin/engine/table/AbstractPageFrameRecordCursorFactory.javacore/src/main/java/io/questdb/griffin/engine/table/AbstractTreeSetRecordCursorFactory.javacore/src/main/java/io/questdb/std/AbstractCharSequenceHashSet.javacore/src/main/java/io/questdb/std/Mutable.javacore/src/main/java/io/questdb/std/datetime/AbstractDateFormat.javacore/src/main/java/io/questdb/std/datetime/DateFormat.javacore/src/main/java/io/questdb/std/str/AbstractCharSequence.javacore/src/main/java/io/questdb/std/str/CloneableMutable.javacore/src/test/java/io/questdb/test/ServerMainForeignTableTest.javacore/src/test/java/io/questdb/test/ServerMainTest.javacore/src/test/java/io/questdb/test/cairo/MetadataCacheTest.javacore/src/test/java/io/questdb/test/cairo/pool/RecentWriteTrackerIntegrationTest.javacore/src/test/java/io/questdb/test/cairo/pool/RecentWriteTrackerTest.javacore/src/test/java/io/questdb/test/cairo/pool/WriterPoolTest.javacore/src/test/java/io/questdb/test/cairo/wal/WalTableSqlTest.javacore/src/test/java/io/questdb/test/cairo/wal/WalWriterTest.javacore/src/test/java/io/questdb/test/cutlass/pgwire/PGJobContextTest.javacore/src/test/java/io/questdb/test/griffin/KeywordAsTableNameTest.javacore/src/test/java/io/questdb/test/griffin/ShowTablesTest.javacore/src/test/java/io/questdb/test/griffin/engine/functions/catalogue/TablesBootstrapTest.javacore/src/test/java/io/questdb/test/griffin/engine/functions/catalogue/TablesFunctionFactoryTest.java
💤 Files with no reviewable changes (2)
- core/src/main/java/io/questdb/cairo/wal/seq/SeqTxnTracker.java
- core/src/main/java/io/questdb/cairo/mv/WalTxnRangeLoader.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-10T14:28:48.329Z
Learnt from: mtopolnik
Repo: questdb/questdb PR: 0
File: :0-0
Timestamp: 2025-11-10T14:28:48.329Z
Learning: In AsOfJoinDenseRecordCursorFactoryBase.java, the `backwardScanExhausted` flag is intentionally NOT reset in `toTop()` because backward scan results are reusable across cursor rewinds. The backward scan caches historical matches that remain valid when the cursor is rewound.
Applied to files:
core/src/test/java/io/questdb/test/griffin/ShowTablesTest.javacore/src/main/java/io/questdb/cairo/sql/RecordCursorFactory.javacore/src/main/java/io/questdb/griffin/engine/functions/catalogue/TablesFunctionFactory.javacore/src/main/java/io/questdb/griffin/engine/join/AbstractAsOfJoinFastRecordCursor.java
📚 Learning: 2025-11-07T00:59:31.522Z
Learnt from: bluestreak01
Repo: questdb/questdb PR: 0
File: :0-0
Timestamp: 2025-11-07T00:59:31.522Z
Learning: In QuestDB's Cairo engine, transaction (_txn) files have a strong invariant: they are never truncated below TX_BASE_HEADER_SIZE. Once created, they are either fully formed (size >= header size) or completely removed along with the entire table directory when the table is dropped.
Applied to files:
core/src/main/java/io/questdb/cairo/wal/CheckWalTransactionsJob.java
🧬 Code graph analysis (7)
core/src/test/java/io/questdb/test/ServerMainTest.java (6)
core/src/main/java/io/questdb/PropBootstrapConfiguration.java (1)
PropBootstrapConfiguration(29-44)core/src/main/java/io/questdb/ServerMain.java (1)
ServerMain(64-521)core/src/main/java/io/questdb/cairo/TableToken.java (1)
TableToken(38-192)core/src/main/java/io/questdb/cairo/pool/RecentWriteTracker.java (1)
RecentWriteTracker(73-1139)core/src/main/java/io/questdb/cairo/security/AllowAllSecurityContext.java (1)
AllowAllSecurityContext(35-209)core/src/main/java/io/questdb/std/ObjList.java (1)
ObjList(34-395)
benchmarks/src/main/java/org/questdb/RecentWriteTrackerBenchmark.java (2)
core/src/main/java/io/questdb/cairo/TableToken.java (1)
TableToken(38-192)core/src/main/java/io/questdb/cairo/pool/RecentWriteTracker.java (1)
WriteStats(567-1138)
core/src/main/java/io/questdb/griffin/engine/functions/GroupByFunction.java (1)
core/rust/qdbr/src/allocator.rs (1)
allocator(351-357)
core/src/main/java/io/questdb/cairo/wal/ApplyWal2TableJob.java (1)
core/src/main/java/io/questdb/std/Numbers.java (1)
Numbers(44-3523)
core/src/main/java/io/questdb/cairo/wal/WalWriter.java (2)
core/src/main/java/io/questdb/cairo/pool/RecentWriteTracker.java (1)
RecentWriteTracker(73-1139)core/src/main/java/io/questdb/std/Numbers.java (1)
Numbers(44-3523)
core/src/test/java/io/questdb/test/cutlass/pgwire/PGJobContextTest.java (1)
compat/src/test/php/runner.php (1)
executeQuery(36-89)
core/src/main/java/io/questdb/griffin/engine/functions/catalogue/TablesFunctionFactory.java (5)
core/src/main/java/io/questdb/cairo/pool/RecentWriteTracker.java (1)
RecentWriteTracker(73-1139)core/src/main/java/io/questdb/cairo/wal/seq/SeqTxnTracker.java (1)
SeqTxnTracker(34-193)core/src/main/java/io/questdb/cairo/wal/seq/TableSequencerAPI.java (1)
TableSequencerAPI(55-564)core/src/main/java/io/questdb/std/Numbers.java (1)
Numbers(44-3523)core/src/main/java/io/questdb/std/ObjList.java (1)
ObjList(34-395)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (35)
- GitHub Check: New pull request (Coverage Report Coverage Report)
- GitHub Check: New pull request (Hosted Running tests on windows-other-2)
- GitHub Check: New pull request (Hosted Running tests on windows-other-1)
- GitHub Check: New pull request (Hosted Running tests on windows-pgwire)
- GitHub Check: New pull request (Hosted Running tests on windows-cairo-2)
- GitHub Check: New pull request (Hosted Running tests on windows-cairo-1)
- GitHub Check: New pull request (Hosted Running tests on windows-fuzz2)
- GitHub Check: New pull request (SelfHosted Running tests with cover on linux-other)
- GitHub Check: New pull request (Hosted Running tests on windows-fuzz1)
- GitHub Check: New pull request (SelfHosted Running tests with cover on linux-pgwire)
- GitHub Check: New pull request (Hosted Running tests on windows-griffin-sub)
- GitHub Check: New pull request (SelfHosted Running tests with cover on linux-cairo-sub)
- GitHub Check: New pull request (Hosted Running tests on windows-griffin-base)
- GitHub Check: New pull request (SelfHosted Running tests with cover on linux-cairo-root)
- GitHub Check: New pull request (Hosted Running tests on mac-other)
- GitHub Check: New pull request (SelfHosted Running tests with cover on linux-fuzz2)
- GitHub Check: New pull request (Hosted Running tests on mac-pgwire)
- GitHub Check: New pull request (SelfHosted Running tests with cover on linux-fuzz1)
- GitHub Check: New pull request (Hosted Running tests on mac-cairo-fuzz)
- GitHub Check: New pull request (SelfHosted Running tests with cover on linux-griffin-sub)
- GitHub Check: New pull request (Hosted Running tests on mac-cairo)
- GitHub Check: New pull request (SelfHosted Running tests with cover on linux-griffin-root)
- GitHub Check: New pull request (Rust Test and Lint on linux-jdk17)
- GitHub Check: New pull request (Hosted Running tests on mac-griffin)
- GitHub Check: New pull request (SelfHosted Griffin tests on linux-x86-graal)
- GitHub Check: New pull request (SelfHosted Griffin tests on linux-arm64)
- GitHub Check: New pull request (SelfHosted Cairo tests on linux-x64-zfs)
- GitHub Check: New pull request (SelfHosted Griffin tests on linux-x64-zfs)
- GitHub Check: New pull request (SelfHosted Cairo tests on linux-x86-graal)
- GitHub Check: New pull request (SelfHosted Cairo tests on linux-arm64)
- GitHub Check: New pull request (SelfHosted Other tests on linux-x86-graal)
- GitHub Check: New pull request (SelfHosted Other tests on linux-arm64)
- GitHub Check: New pull request (SelfHosted Other tests on linux-x64-zfs)
- GitHub Check: New pull request (Trigger Enterprise CI Trigger Enterprise Pipeline)
- GitHub Check: New pull request (Check Changes Check changes)
[PR Coverage check]😍 pass : 414 / 553 (74.86%) file detail
|
tandem: https://github.com/questdb/questdb-enterprise/pull/841
documentation: questdb/documentation#310
Summary
Adds real-time table write statistics tracking, exposed via new columns in the
tables()SQL function.New columns in
tables():suspendedfalsefor non-WAL tables)rowCountpendingRowCountdedupeRowCountlastWriteTimestampwriterTxnsequencerTxnlastWalTimestampmemoryPressureLevelnullfor non-WAL tablestxnCounttxnSizeP50txnSizeP90txnSizeP99txnSizeMaxwriteAmplificationCountwriteAmplificationP50writeAmplificationP90writeAmplificationP99writeAmplificationMaxmergeThroughputCountmergeThroughputP50mergeThroughputP90mergeThroughputP99mergeThroughputMaxreplicaBatchCountreplicaBatchSizeP50replicaBatchSizeP90replicaBatchSizeP99replicaBatchSizeMaxreplicaMorePendingtrueif last download batch was limited and more data is availablemaxUncommittedRowso3MaxLagThroughput percentile semantics
For throughput metrics (where higher = better), percentiles are inverted to show the slow tail:
mergeThroughputP99= throughput that 99% of jobs exceeded (i.e., the slowest 1%)mergeThroughputP90= throughput that 90% of jobs exceeded (i.e., the slowest 10%)This is consistent with latency percentiles where P99 shows the "worst" case.
Replica columns
The
replicaBatchCount,replicaBatchSize*, andreplicaMorePendingcolumns are populated on replicas only viarecordReplicaDownload(). On primaries, these columns will be 0 orfalse. This separation prevents replica download batches (which may contain multiple transactions) from corrupting thetxnSize*histogram statistics.Column order
The full column order for
tables()is now:Nullability and precision
These values are approximations, not precise real-time metrics:
nullfor tables that haven't been written to since server start, or have been evicted from the tracker due to capacity limitspendingRowCount,dedupeRowCount,sequencerTxn,lastWalTimestamp, histogram stats, write amplification, and merge throughput are updated on every WAL commit/applyTxReader), but diverge as writes occurNon-WAL tables:
sequencerTxn,lastWalTimestamp,pendingRowCount,dedupeRowCount,memoryPressureLevel, histogram columns, write amplification, and merge throughput columns are alwaysnullor 0.suspendedis alwaysfalse.WAL tables: All columns populated when tracked.
lastWalTimestampreflects the max data timestamp from the WAL transaction, not wall-clock time.Example usage:
Implementation
RecentWriteTracker
New concurrent data structure (
io.questdb.cairo.pool.RecentWriteTracker) optimized for:ConcurrentHashMapfor zero-contention lookupsWriteStatsobjects via atomic field updatesmax(writerTimestamp, walTimestamp)sequencerTxnandwalTimestamp(highest value wins)LongAdderforpendingRowCountanddedupeRowCountSimpleReadWriteLockfor concurrent accessrecordReplicaDownload())WAL Row Tracking
physicalRowsAdded / rowsAddedratio after WAL apply, exposes percentiles and maxrowsAdded * 1000000 / insertTimespan(rows/second) after WAL merge, exposes inverted percentiles (P99 = 1st percentile = slow tail)Replica-specific tracking
walRowCount(same asrecordWalWrite)batchSizeHistogram(NOTtxnSizeHistogram)sequencerTxnandwalTimestamp(highest wins)replicaMorePendingflag indicating if more data is availableIntegration points
TxReaderon startupsuspendedstatus andmemoryPressureLevelviaSeqTxnTrackerConfiguration
cairo.recent.write.tracker.capacity(default: 1000) - maximum number of tables trackedTest plan
tables()columns🤖 Generated with Claude Code