Automated testing to find logic and performance bugs in database systems
SQLancer is a tool to automatically test Database Management Systems (DBMSs) in order to find bugs in their implementation. That is, it finds bugs in the code of the DBMS implementation, rather than in queries written by the user. SQLancer has found hundreds of bugs in mature and widely-known DBMSs.
SQLancer tackles two essential challenges when automatically testing the DBMSs:
Minimum Requirements:
The following commands clone SQLancer, create a JAR, and start SQLancer to test SQLite using Non-optimizing Reference Engine Construction (NoREC):
git clone https://github.com/sqlancer/sqlancer
cd sqlancer
mvn package -DskipTests
cd target
java -jar sqlancer-*.jar --num-threads 4 sqlite3 --oracle NoREC
Running and terminating. If the execution prints progress information every five seconds, then the tool works as expected. The shortcut CTRL+C can be used to terminate SQLancer manually. If SQLancer does not find any bugs, it executes infinitely. The option --num-tries
can be used to control after how many bugs SQLancer terminates. Alternatively, the option --timeout-seconds
can be used to specify the maximum duration that SQLancer is allowed to run.
Parameters. If you launch SQLancer without parameters, available options and commands are displayed. Note that general options that are supported by all DBMS-testing implementations (e.g., --num-threads
) need to precede the name of the DBMS to be tested (e.g., sqlite3
). Options that are supported only for specific DBMS (e.g., --test-rtree
for SQLite3), or options for which each testing implementation provides different values (e.g. --oracle NoREC
) need to go after the DBMS name.
DBMSs. To run SQLancer on SQLite, it was not necessary to install and set up a DBMS. The reason for this is that embedded DBMSs run in the same process as the application and thus require no separate installation or setup. Embedded DBMSs supported by SQLancer include DuckDB, H2, and SQLite. Their binaries are included as JAR dependencies. Note that any crashes in these systems will also cause a crash in the JVM on which SQLancer runs.
Logs. SQLancer stores logs in the target/logs
subdirectory. By default, the option --log-each-select
is enabled, which results in every SQL statement that is sent to the DBMS being logged. The corresponding file names are postfixed with -cur.log
. In addition, if SQLancer detects a logic bug, it creates a file with the extension .log
, in which the statements to reproduce the bug are logged, including only the last query that was executed along with the other statements to set up the database state.
Reducing bugs. After finding a bug-inducing test input, the input typically needs to be reduced to be further analyzed, as it might contain many SQL statements that are redundant to reproduce the bug. One option is to do this manually, by removing a statement or feature at a time, replaying the bug-inducing statements, and applying the test oracle (e.g., for test oracles like TLP or NoREC, this would require checking that both queries still produce a different result). This process can be automated using a so-called delta-debugging approach. SQLancer includes an experimental implementation of a delta debugging approach, which can be enabled using --use-reducer
. In the past, we have successfully used C-Reduce, which requires specifying the test oracle in a script that can be executed by C-Reduce.
Testing the latest DBMS version. For most DBMSs, SQLancer supports only a previous release version. Thus, potential bugs that SQLancer finds could be already fixed in the latest development version of the DBMS. If you are not a developer of the DBMS that you are testing, we would like to encourage you to validate that the bug can still be reproduced before reporting it. We would appreciate it if you could mention SQLancer when you report bugs found by it. We would also be excited to hear about your experience using SQLancer or related use cases or extensions.
Options. SQLancer provides many options that you can use to customize its behavior. Executing java -jar sqlancer-*.jar --help
will list them and should print output such as the following:
Usage: SQLancer [options] [command] [command options]
Options:
--ast-reducer-max-steps
EXPERIMENTAL Maximum steps the AST-based reducer will do
Default: -1
--ast-reducer-max-time
EXPERIMENTAL Maximum time duration (secs) the statement reducer will do
Default: -1
--canonicalize-sql-strings
Should canonicalize query string (add ';' at the end
Default: true
--constant-cache-size
Specifies the size of the constant cache. This option only takes effect
when constant caching is enabled
Default: 100
...
Which SQLancer version to use. The recommended way to use SQLancer is to use its latest source version on GitHub. Infrequent and irregular official releases are also available on the following platforms:
Understanding SQL generation. To analyze bug-inducing statements, it is helpful to understand the characteristics of SQLancer. First, SQLancer is expected to always generate SQL statements that are syntactically valid for the DBMS under test. Thus, you should never observe any syntax errors. Second, SQLancer might generate statements that are semantically invalid. For example, SQLancer might attempt to insert duplicate values into a column with a UNIQUE
constraint, as completely avoiding such semantic errors is challenging. Third, any bug reported by SQLancer is expected to be a real bug, except those reported by CERT (as performance issues are not as clearly defined as other kinds of bugs). If you observe any bugs indicated by SQLancer that you do not consider bugs, something is likely wrong with your setup. Finally, related to the aforementioned point, SQLancer is specific to a version of the DBMS, and you can find the version against which we are tested in our GitHub Actions workflow. If you are testing against another version, you might observe various false alarms (e.g., caused by syntax errors). While we would always like for SQLancer to be up-to-date with the latest development version of each DBMS, we lack the resources to achieve this.
Approach | Description |
---|---|
Pivoted Query Synthesis (PQS) | PQS is the first technique that we designed and implemented. It randomly selects a row, called a pivot row, for which a query is generated that is guaranteed to fetch the row. If the row is not contained in the result set, a bug has been detected. It is fully described here. PQS is the most powerful technique, but also requires more implementation effort than the other two techniques. It is currently unmaintained. |
Non-optimizing Reference Engine Construction (NoREC) | NoREC aims to find optimization bugs. It is described here. It translates a query that is potentially optimized by the DBMS to one for which hardly any optimizations are applicable, and compares the two result sets. A mismatch between the result sets indicates a bug in the DBMS. |
Ternary Logic Partitioning (TLP) | TLP partitions a query into three partitioning queries, whose results are composed and compare to the original query’s result set. A mismatch in the result sets indicates a bug in the DBMS. In contrast to NoREC and PQS, it can detect bugs in advanced features such as aggregate functions. |
Cardinality Estimation Restriction Testing (CERT) | CERT aims to find performance issues through unexpected estimated cardinalities, which represent the estimated number of returned rows. It is described here. It derives a query to a more restrict query, whose estimated cardinality should be no more than that for the original query. An violation indicates a potential performance issue. CERT supports TiDB, CockroachDB, and MySQL. |
Differential Query Plans (DQP) | DQP aims to find logic bugs in database systems by checking whether the query plans of the same query perform consistently. It is described here. DQP supports MySQL, MariaDB, and TiDB. |
Approach | Description |
---|---|
Random Generation | Random generation is the default test case generation approach in SQLancer. First, random tables are generated. Then queries are randomly generated based on the schemas of the tables. |
Query Plan Guidance (QPG) | QPG is a test case generation method guided by query plan coverage. Given a database state, we mutate it after no new unique query plans have been observed by randomly-generated queries on the database state aiming to cover more unique query plans for exposing more logics of DBMSs. This approach is enabled by option --qpg-enable and now supports TLP and NoREC oracles for SQLite, CockroachDB, TiDB, and Materialize. |
Please find the .bib
entries here.
Since SQL dialects differ widely, each DBMS to be tested requires a separate implementation.
DBMS | Status | Expression Generation | Description |
---|---|---|---|
SQLite | Working | Untyped | This implementation is currently affected by a significant performance regression that still needs to be investigated |
MySQL | Working | Untyped | Running this implementation likely uncovers additional, unreported bugs. |
PostgreSQL | Working | Typed | |
Citus (PostgreSQL Extension) | Working | Typed | This implementation extends the PostgreSQL implementation of SQLancer, and was contributed by the Citus team. |
MariaDB | Preliminary | Untyped | The implementation of this DBMS is very preliminary, since we stopped extending it after all but one of our bug reports were addressed. Running it likely uncovers additional, unreported bugs. |
CockroachDB | Working | Typed | |
TiDB | Working | Untyped | |
DuckDB | Working | Untyped, Generic | |
ClickHouse | Preliminary | Untyped, Generic | Implementing the different table engines was not convenient, which is why only a very preliminary implementation exists. |
TDEngine | Removed | Untyped | We removed the TDEngine implementation since all but one of our bug reports were still unaddressed five months after we reported them. |
OceanBase | Working | Untyped | |
YugabyteDB | Working | Typed (YSQL), Untyped (YCQL) | YSQL implementation based on Postgres code. YCQL implementation is primitive for now and uses Cassandra JDBC driver as a proxy interface. |
Databend | Working | Typed | |
QuestDB | Working | Untyped, Generic | The implementation of QuestDB is still WIP, current version covers very basic data types, operations and SQL keywords. |
CnosDB | Working | Typed | The implementation of CnosDB currently uses Restful API. |
Materialize | Working | Typed | |
Apache Doris | Preliminary | Typed | This is a preliminary implementation, which only contains the common logic of Doris. We have found some errors through it, and hope to improve it in the future. |
Presto | Preliminary | Typed | This is a preliminary implementation, only basic types supported. |
DataFusion | Preliminary | Typed | Only basic SQL features are supported. |
Some DBMS were once supported but subsequently removed.
DBMS | Pull Request | Description |
---|---|---|
ArangoDB | #915 | This implementation was removed because ArangoDB is a NoSQL DBMS, while the majority were SQL DBMSs, which resulted in difficulty refactoring SQLancer. |
Cosmos | #915 | This implementation was removed because Cosmos is a NoSQL DBMS, while the majority were SQL DBMSs, which resulted in difficulty refactoring SQLancer. |
MongoDB | #915 | This implementation was removed because MongoDB is a NoSQL DBMS, while the majority were SQL DBMSs, which resulted in difficulty refactoring SQLancer. |
StoneDB | #963 | This implementation was removed because development of StoneDB stopped. |
We have created a Slack workspace to discuss SQLancer, and DBMS testing in general. SQLancer’s official Twitter handle is @sqlancer_dbms.
In many cases, SQLancer does not support the latest version of a DBMS. You can check the .github/workflows/main.yml
file to determine which version we use in our CI tests, which corresponds to the currently supported version of that DBMS. SQLancer should print only an AssertionError
and produce a corresponding log file, if it has identified a bug. To upgrade SQLancer to support a new DBMS version, either two options are advisable: (1) the generators can be updated to no longer generate certain patterns that might cause errors (e.g., which might be the case if a keyword or option is no longer supported) or (2) the newly-appearing errors can be added as expected errors so that SQLancer ignores them when they appear (e.g., this is useful if some error-inducing patterns cannot easily be avoided).
Another reason for many failures on a supported version could be that error messages are printed in a non-English locale (which would then be visible in the stack trace). In such a case, try setting the DBMS’ locale to English (e.g., see the PostgreSQL homepage).
For some DBMSs, SQLancer expects that a database “test” exists, which it then uses as an initial database to connect to. If you have not yet created such a database, you can use a command such as CREATE DATABASE test
to create this database (e.g., see the PostgreSQL documentation).