Documentation Index
Fetch the complete documentation index at: https://mintlify.com/questdb/questdb/llms.txt
Use this file to discover all available pages before exploring further.
Overview
QuestDB has over 4,000 tests, most of which are integration tests that start a server, interact with it, and assert outcomes. All contributions must include comprehensive tests.
Running Tests
Basic Test Commands
# Run all tests
mvn test
# Run a specific test class
mvn -Dtest=ClassNameTest test
# Run a specific test method
mvn -Dtest=ClassNameTest#methodName test
Important: Do not run multiple mvn test commands in parallel — each invocation triggers a full build and they interfere with each other. Run test commands sequentially.
Test Execution Time
The full test suite completes within 2-6 minutes depending on your system. This includes:
- Unit tests
- Integration tests
- SQL correctness tests
- Protocol tests (ILP, PostgreSQL wire, HTTP)
- Memory leak detection tests
Test Structure Requirements
Memory Leak Detection
All tests must use assertMemoryLeak() unless they are narrow unit tests that provably don’t allocate any native memory.
@Test
public void testTableWriter() throws Exception {
assertMemoryLeak(() -> {
// Your test code here
});
}
Why: QuestDB uses off-heap memory extensively. Memory leaks are a critical concern. assertMemoryLeak() verifies that all native memory allocated during the test is properly freed.
Resource Cleanup
Resource leaks are a pain point in QuestDB. Always:
- Think carefully about all possible code paths, especially error paths
- Write tests that ensure correct resource cleanup on each path
- Test failure scenarios (e.g., disk full, invalid input, network errors)
- Verify that resources are released in finally blocks or try-with-resources
SQL Test Conventions
Query Assertions
Use assertQueryNoLeakCheck() to assert query results:
assertQueryNoLeakCheck(
"column1\tcolumn2\n" +
"value1\tvalue2\n",
"SELECT column1, column2 FROM table",
null,
true,
true
);
DDL Execution
Use execute() for non-queries (DDL statements):
execute("CREATE TABLE my_table (ts TIMESTAMP, value INT) timestamp(ts)");
SQL Style Guidelines
- Use UPPERCASE for SQL keywords:
// Good
execute("CREATE TABLE trades (ts TIMESTAMP, symbol SYMBOL, price DOUBLE) timestamp(ts)");
// Bad
execute("create table trades (ts timestamp, symbol symbol, price double) timestamp(ts)");
- Use a single INSERT statement for multiple rows:
// Good
execute(
"INSERT INTO trades VALUES" +
"('2024-01-01T00:00:00.000000Z', 'AAPL', 150.0)," +
"('2024-01-01T00:00:01.000000Z', 'AAPL', 150.5)," +
"('2024-01-01T00:00:02.000000Z', 'GOOGL', 2800.0)"
);
// Bad - multiple INSERT statements
execute("INSERT INTO trades VALUES ('2024-01-01T00:00:00.000000Z', 'AAPL', 150.0)");
execute("INSERT INTO trades VALUES ('2024-01-01T00:00:01.000000Z', 'AAPL', 150.5)");
- Use multiline strings for longer statements:
assertQueryNoLeakCheck(
"ts\tsymbol\tprice\n" +
"2024-01-01T00:00:00.000000Z\tAAPL\t150.0\n" +
"2024-01-01T00:00:01.000000Z\tAAPL\t150.5\n",
"""
SELECT ts, symbol, price
FROM trades
WHERE symbol = 'AAPL'
ORDER BY ts
""",
"ts",
true,
true
);
- Use underscore to separate thousands in numbers with 5+ digits:
// Good
long count = 1_000_000;
long rows = 50_000;
// Bad
long count = 1000000;
long rows = 50000;
Note: QuestDB SQL supports underscores as thousands separators: SELECT * FROM table WHERE value > 1_000_000. The Numbers.parseInt() / parseLong() methods already support underscore separators.
QuestDB SQL Dialect Notes
When writing tests, be aware of QuestDB’s SQL dialect:
Type Casting
QuestDB supports expr::TYPE syntax. Prefer it to CAST(expr, type):
// Good
assertQuery("SELECT price::INT FROM trades");
// Less idiomatic
assertQuery("SELECT CAST(price, INT) FROM trades");
Arrays
QuestDB supports multidimensional arrays. Dimensionality is encoded in the column type:
execute("CREATE TABLE arrays (doubles DOUBLE[], matrix DOUBLE[][])");
// DOUBLE[] and DOUBLE[][] are distinct types
No DELETE Statement
QuestDB does not support DELETE. Rows can only be soft-deleted through application logic (e.g., a deleted BOOLEAN column).
QuestDB does support ALTER TABLE DROP PARTITION for mass data deletion:
execute("ALTER TABLE trades DROP PARTITION LIST '2024-01-01'");
Underscores in Numeric Literals
Always use underscores in numbers with 5+ digits in test code and implementation:
SELECT * FROM trades WHERE volume > 1_000_000
Test Naming Conventions
Test Class Names
Test classes should end with Test:
public class TableWriterTest { }
public class SqlCompilerTest { }
Test Method Names
Use descriptive names that explain what is being tested:
@Test
public void testInsertOutOfOrderData() { }
@Test
public void testCreateTableWithTimestamp() { }
@Test
public void testSelectWithWhereClause() { }
For changes that may impact performance:
- Benchmark before and after your changes
- Use row generator functions to create large datasets:
CREATE TABLE perf_test AS (
SELECT
x AS id,
timestamp_sequence(0, 1000000) AS ts,
rnd_double() AS value
FROM long_sequence(10_000_000)
);
- Include performance data in PR:
- Data volume tested (e.g., “10M rows”)
- Before/after timings
- Hardware specs (CPU, RAM)
- Any tradeoffs or regressions observed
Testing Best Practices
Test Error Paths
Don’t just test the happy path. Test error conditions:
@Test
public void testInsertInvalidTimestamp() throws Exception {
assertMemoryLeak(() -> {
execute("CREATE TABLE trades (ts TIMESTAMP, price DOUBLE) timestamp(ts)");
try {
execute("INSERT INTO trades VALUES ('invalid', 100.0)");
Assert.fail("Expected exception");
} catch (SqlException e) {
TestUtils.assertContains(e.getFlyweightMessage(), "Invalid timestamp");
}
});
}
Test Null Handling
Always consider NULL behavior when dealing with column data, expression results, and SQL statements:
@Test
public void testNullValues() throws Exception {
assertMemoryLeak(() -> {
execute("CREATE TABLE nulls (id INT, value INT)");
execute("INSERT INTO nulls VALUES (1, null), (2, 100)");
assertQueryNoLeakCheck(
"id\tvalue\n" +
"1\tNaN\n" + // INT NULL displays as NaN
"2\t100\n",
"SELECT id, value FROM nulls",
null,
true,
true
);
});
}
Distinguish:
- NULL as a sentinel value (“not initialized yet”)
- NULL as an actual SQL NULL value
Test Concurrent Access
For writer/reader code, test concurrent access patterns:
@Test
public void testConcurrentReaderWriter() throws Exception {
assertMemoryLeak(() -> {
// Create writer thread
// Create reader threads
// Verify readers see consistent data
// Verify no deadlocks or race conditions
});
}
Test Resource Limits
Test behavior under resource constraints:
@Test
public void testDiskFull() throws Exception {
// Mock file system to simulate disk full
// Verify graceful handling
// Verify cleanup on failure
}
Common Test Utilities
TestUtils
The TestUtils class provides common assertions:
TestUtils.assertContains(message, "expected substring");
TestUtils.assertEquals(expected, actual);
TestUtils.assertMemoryLeak(() -> { /* test code */ });
Creating Test Data
Use QuestDB’s row generator functions:
-- Generate sequence of longs
SELECT * FROM long_sequence(1000);
-- Generate timestamps
SELECT timestamp_sequence(0, 1000000) FROM long_sequence(100);
-- Generate random data
SELECT
rnd_symbol('A', 'B', 'C') AS symbol,
rnd_double() AS price,
rnd_int(1, 1000, 0) AS volume
FROM long_sequence(1000);
Test Organization
Tests are organized by component:
core/src/test/java/io/questdb/
├── cairo/ # Storage engine tests
├── griffin/ # SQL compiler and execution tests
├── cutlass/
│ ├── http/ # HTTP API tests
│ ├── line/ # ILP tests
│ └── pgwire/ # PostgreSQL wire protocol tests
├── std/ # Utility and collection tests
└── ... # Other component tests
Continuous Integration
All tests run on CI for every PR:
- Multiple OS: Linux, Windows, macOS
- Multiple Java versions: Java 17 on all platforms
- Different architectures: x86-64, ARM64 (where applicable)
Your responsibility: Monitor your PR’s CI status. We will not always remind you. PRs with failing CI for more than 2 weeks will be closed.
Error Position Convention
When throwing SqlException, the position should point to the specific offending character, not the start of the expression:
// Good
throw SqlException.$(position, "Invalid column name");
// Position points to the actual error location in the SQL string
Related Pages