diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..a217b347e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+- package-ecosystem: maven
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 000000000..9f0dbba32
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,701 @@
+name: ci
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'src/**'
+ - 'test/**'
+ - '.github/workflows/*.yml'
+ - 'pom.xml'
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'src/**'
+ - 'test/**'
+ - '.github/workflows/*.yml'
+ - 'pom.xml'
+
+jobs:
+ misc:
+ name: General tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Verify
+ run: mvn -B verify -DskipTests=true
+ - name: Misc Tests
+ run: mvn -Djacoco.skip=true -B '-Dtest=!sqlancer.dbms.**,!sqlancer.qpg.**' test
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ - name: Naming Convention Tests
+ run: python src/check_names.py
+
+ citus:
+ name: DBMS Tests (Citus)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Set up Citus
+ run: |
+ echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
+ curl https://install.citusdata.com/community/deb.sh | sudo bash
+ sudo sed -i 's/noble/jammy/g' /etc/apt/sources.list.d/citusdata_community.list # https://github.com/citusdata/citus/issues/7692
+ sudo apt-get update
+ sudo apt-get -y install postgresql-17-citus-13.0
+ sudo chown -R $USER:$USER /var/run/postgresql
+ export PATH=/usr/lib/postgresql/17/bin:$PATH
+ cd ~
+ mkdir -p citus/coordinator citus/worker1 citus/worker2
+ initdb -D citus/coordinator
+ initdb -D citus/worker1
+ initdb -D citus/worker2
+ echo "shared_preload_libraries = 'citus'" >> citus/coordinator/postgresql.conf
+ echo "shared_preload_libraries = 'citus'" >> citus/worker1/postgresql.conf
+ echo "shared_preload_libraries = 'citus'" >> citus/worker2/postgresql.conf
+ pg_ctl -D citus/coordinator -o "-p 9700" -l coordinator_logfile start || cat coordinator_logfile || cat citus/coordinator/coordinator_logfile
+ pg_ctl -D citus/worker1 -o "-p 9701" -l worker1_logfile start
+ ls citus/worker1
+ pg_ctl -D citus/worker2 -o "-p 9702" -l worker2_logfile start
+ psql -c "CREATE ROLE sqlancer SUPERUSER LOGIN CREATEDB PASSWORD 'sqlancer';" -p 9700 -d postgres -U $USER
+ createdb test -p 9700 -U $USER
+ psql -c "CREATE ROLE sqlancer SUPERUSER LOGIN CREATEDB PASSWORD 'sqlancer';" -p 9701 -d postgres -U $USER
+ createdb test -p 9701 -U $USER
+ psql -c "CREATE ROLE sqlancer SUPERUSER LOGIN CREATEDB PASSWORD 'sqlancer';" -p 9702 -d postgres -U $USER
+ createdb test -p 9702 -U $USER
+ psql -c "CREATE EXTENSION citus;" -p 9700 -U $USER -d test
+ psql -c "CREATE EXTENSION citus;" -p 9701 -U $USER -d test
+ psql -c "CREATE EXTENSION citus;" -p 9702 -U $USER -d test
+ psql -c "SELECT * from citus_add_node('localhost', 9701);" -p 9700 -U $USER -d test
+ psql -c "SELECT * from citus_add_node('localhost', 9702);" -p 9700 -U $USER -d test
+ - name: Run Tests
+ run: CITUS_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestCitus test
+
+ clickhouse:
+ name: DBMS Tests (ClickHouse)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Set up ClickHouse
+ run: |
+ docker pull clickhouse/clickhouse-server:24.3.1.2672
+ docker run --ulimit nofile=262144:262144 --name clickhouse-server -p8123:8123 -d clickhouse/clickhouse-server:24.3.1.2672
+ until curl -sf http://127.0.0.1:8123/ping 2>/dev/null; do sleep 1; done
+ - name: Run Tests
+ run: CLICKHOUSE_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=ClickHouseBinaryComparisonOperationTest,TestClickHouse,ClickHouseOperatorsVisitorTest,ClickHouseToStringVisitorTest test
+ - name: Show fatal errors
+ run: docker exec clickhouse-server grep Fatal /var/log/clickhouse-server/clickhouse-server.log || echo No Fatal Errors found
+ - name: Teardown ClickHouse server
+ run: |
+ docker stop clickhouse-server
+ docker rm clickhouse-server
+
+ cockroachdb:
+ name: DBMS Tests (CockroachDB)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Set up CockroachDB
+ run: |
+ wget -qO- https://binaries.cockroachdb.com/cockroach-v24.2.0.linux-amd64.tgz | tar xvz
+ cd cockroach-v24.2.0.linux-amd64/ && ./cockroach start-single-node --insecure &
+ until cockroach-v24.2.0.linux-amd64/cockroach sql --insecure -e "SELECT 1" 2>/dev/null; do sleep 2; done
+ - name: Create SQLancer user
+ run: cd cockroach-v24.2.0.linux-amd64/ && ./cockroach sql --insecure -e "CREATE USER sqlancer; GRANT admin to sqlancer" && cd ..
+ - name: Run Tests
+ run: |
+ COCKROACHDB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestCockroachDBNoREC test
+ COCKROACHDB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestCockroachDBTLP test
+ COCKROACHDB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestCockroachDBCERT test
+
+ cockroachdb-qpg:
+ name: QPG Tests (CockroachDB)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Set up CockroachDB
+ run: |
+ wget -qO- https://binaries.cockroachdb.com/cockroach-v24.2.0.linux-amd64.tgz | tar xvz
+ cd cockroach-v24.2.0.linux-amd64/ && ./cockroach start-single-node --insecure &
+ until cockroach-v24.2.0.linux-amd64/cockroach sql --insecure -e "SELECT 1" 2>/dev/null; do sleep 2; done
+ - name: Create SQLancer user
+ run: cd cockroach-v24.2.0.linux-amd64/ && ./cockroach sql --insecure -e "CREATE USER sqlancer; GRANT admin to sqlancer" && cd ..
+ - name: Run Tests
+ run: COCKROACHDB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestCockroachDBQPG test
+
+ databend:
+ name: DBMS Tests (Databend)
+ runs-on: ubuntu-latest
+ services:
+ databend:
+ image: datafuselabs/databend:v1.2.900-nightly
+ env:
+ QUERY_DEFAULT_USER: sqlancer
+ QUERY_DEFAULT_PASSWORD: sqlancer
+ ports:
+ - 8000:8000
+ - 3307:3307
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: |
+ DATABEND_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestDatabendTLP test
+ DATABEND_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestDatabendNoREC test
+ DATABEND_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestDatabendPQS test
+
+ datafusion:
+ name: DBMS Tests (DataFusion)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ - name: Cache Rust build
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: src/sqlancer/datafusion/server/datafusion_server
+ - name: Build DataFusion Server
+ run: |
+ cd src/sqlancer/datafusion/server/datafusion_server
+ cargo build
+ - name: Start DataFusion Server
+ run: |
+ cd src/sqlancer/datafusion/server/datafusion_server
+ cargo run &
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Wait for DataFusion Server
+ run: |
+ for i in $(seq 1 30); do
+ if nc -z 127.0.0.1 50051 2>/dev/null; then
+ echo "DataFusion server is ready"
+ exit 0
+ fi
+ echo "Waiting for DataFusion server... ($i/30)"
+ sleep 10
+ done
+ echo "DataFusion server failed to start within 300s"
+ exit 1
+ - name: Run Tests
+ run: |
+ DATAFUSION_AVAILABLE=true mvn -Djacoco.skip=true test -Pdatafusion-tests
+
+ duckdb:
+ name: DBMS Tests (DuckDB)
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build
+ run: mvn -B package -DskipTests=true
+ - name: DuckDB Tests
+ run: |
+ mvn -Djacoco.skip=true -Dtest=TestDuckDBTLP test
+ mvn -Djacoco.skip=true -Dtest=TestDuckDBNoREC test
+
+ h2:
+ name: DBMS Tests (H2)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: mvn -Djacoco.skip=true -Dtest=TestH2 test
+
+ hive:
+ name: DBMS Tests (Hive)
+ runs-on: ubuntu-latest
+ services:
+ metastore:
+ image: apache/hive:4.0.1
+ env:
+ SERVICE_NAME: 'metastore'
+ ports:
+ - 9083:9083
+ volumes:
+ - warehouse:/opt/hive/data/warehouse
+ hiveserver2:
+ image: apache/hive:4.0.1
+ env:
+ SERVICE_NAME: 'hiveserver2'
+ ports:
+ - 10000:10000
+ - 10002:10002
+ volumes:
+ - warehouse:/opt/hive/data/warehouse
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: HIVE_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestHiveTLP test
+
+ spark:
+ name: DBMS Tests (Spark)
+ runs-on: ubuntu-latest
+
+ services:
+ spark:
+ image: apache/spark:3.5.1
+ ports:
+ - 10000:10000
+
+ command: >-
+ /opt/spark/bin/spark-submit
+ --class org.apache.spark.sql.hive.thriftserver.HiveThriftServer2
+ --name "Thrift JDBC/ODBC Server"
+ --master local[*]
+ --driver-memory 4g
+ --conf spark.hive.server2.thrift.port=10000
+ --conf spark.sql.warehouse.dir=/tmp/spark-warehouse
+ spark-internal
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+
+ - name: Run Tests
+ run: SPARK_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestSparkTLP test
+
+ hsqldb:
+ name: DBMS Tests (HSQLDB)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: |
+ mvn -Djacoco.skip=true -Dtest=TestHSQLDBNoREC test
+ mvn -Djacoco.skip=true -Dtest=TestHSQLDBTLP test
+
+ mariadb:
+ name: DBMS Tests (MariaDB)
+ runs-on: ubuntu-latest
+ services:
+ mysql:
+ image: mariadb:11.7.2
+ env:
+ MYSQL_ROOT_PASSWORD: root
+ ports:
+ - 3306:3306
+ options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=10
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Create SQLancer User
+ run: sudo mysql -h 127.0.0.1 -uroot -proot -e "CREATE USER 'sqlancer'@'%' IDENTIFIED BY 'sqlancer'; GRANT ALL PRIVILEGES ON * . * TO 'sqlancer'@'%';"
+ - name: Run Tests
+ run: MARIADB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestMariaDB test
+
+ materialize:
+ name: DBMS Tests (Materialize)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Materialize
+ run: |
+ docker pull materialize/materialized:latest
+ docker run -d -p6875:6875 -p6877:6877 materialize/materialized:latest
+ until pg_isready -h localhost -p 6875 -U materialize; do sleep 1; done
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: |
+ MATERIALIZE_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMaterializeNoREC
+ MATERIALIZE_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMaterializeTLP
+ MATERIALIZE_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMaterializePQS
+
+ materialize-qpg:
+ name: QPG Tests (Materialize)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Materialize
+ run: |
+ docker pull materialize/materialized:latest
+ docker run -d -p6875:6875 -p6877:6877 materialize/materialized:latest
+ until pg_isready -h localhost -p 6875 -U materialize; do sleep 1; done
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: |
+ MATERIALIZE_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMaterializeQPG
+ MATERIALIZE_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMaterializeQueryPlan
+
+ mysql:
+ name: DBMS Tests (MySQL, CERT creation only)
+ runs-on: ubuntu-latest
+ services:
+ mysql:
+ image: mysql:9.7.0
+ env:
+ MYSQL_ROOT_PASSWORD: root
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=10
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Create SQLancer user
+ run: mysql -h 127.0.0.1 -uroot -proot -e "CREATE USER 'sqlancer'@'%' IDENTIFIED BY 'sqlancer'; GRANT ALL PRIVILEGES ON * . * TO 'sqlancer'@'%';"
+ - name: Run Tests
+ run: |
+ MYSQL_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMySQLPQS
+ MYSQL_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMySQLTLP
+ MYSQL_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMySQLCERT
+ MYSQL_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestMySQLDQE
+
+ oceanbase:
+ name: DBMS Tests (OceanBase)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Set up OceanBase
+ run: |
+ docker run -p 2881:2881 --name oceanbase-ce -e MODE=mini -d oceanbase/oceanbase-ce:4.2.1-lts
+ until mysql -h127.1 -uroot@test -P2881 --connect-timeout=3 -Doceanbase -A -e "SELECT 1" 2>/dev/null; do sleep 5; done
+ mysql -h127.1 -uroot@test -P2881 -Doceanbase -A -e"CREATE USER 'sqlancer'@'%' IDENTIFIED BY 'sqlancer'; GRANT ALL PRIVILEGES ON * . * TO 'sqlancer'@'%';"
+ - name: Run Tests
+ run: |
+ OCEANBASE_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestOceanBaseNoREC
+ OCEANBASE_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestOceanBasePQS
+ OCEANBASE_AVAILABLE=true mvn -Djacoco.skip=true test -Dtest=TestOceanBaseTLP
+ postgres:
+ name: DBMS Tests (PostgreSQL)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up PostgreSQL
+ uses: harmon758/postgresql-action@v1.0.0
+ with:
+ postgresql version: '18'
+ postgresql user: 'sqlancer'
+ postgresql password: 'sqlancer'
+ postgresql db: 'test'
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: |
+ POSTGRES_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestPostgresPQS test
+ POSTGRES_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestPostgresTLP test
+ POSTGRES_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestPostgresNoREC test
+ POSTGRES_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestPostgresCERT test
+
+ presto:
+ name: DBMS Tests (Presto)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Set up Presto
+ run: |
+ docker pull prestodb/presto:latest
+ echo "connector.name=memory" >> memory.properties
+ docker run -p 8080:8080 -d -v ./memory.properties:/opt/presto-server/etc/catalog/memory.properties --name presto prestodb/presto:latest
+ until curl -sf http://127.0.0.1:8080/v1/info 2>/dev/null; do sleep 2; done
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: |
+ PRESTO_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestPrestoNoREC test
+ docker restart presto && until curl -sf http://127.0.0.1:8080/v1/info 2>/dev/null; do sleep 2; done
+ PRESTO_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestPrestoTLP test
+ sqlite:
+ name: DBMS Tests (SQLite)
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build
+ run: mvn -B package -DskipTests=true
+ - name: SQLite Tests
+ run: |
+ mvn -Djacoco.skip=true -Dtest=TestSQLitePQS test
+ mvn -Djacoco.skip=true -Dtest=TestSQLiteTLP test
+ mvn -Djacoco.skip=true -Dtest=TestSQLiteNoREC test
+ mvn -Djacoco.skip=true -Dtest=TestSQLiteCODDTest test
+
+ sqlite-qpg:
+ name: QPG Tests (SQLite)
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build
+ run: mvn -B package -DskipTests=true
+ - name: SQLite Tests for QPG
+ run: |
+ mvn -Djacoco.skip=true -Dtest=TestSQLiteQPG test
+
+ tidb:
+ name: DBMS Tests (TiDB, TLP creation only)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Set up TiDB
+ run: |
+ docker pull hawkingrei/tidb-playground:nightly-2025-09-16
+ docker run --name tidb-server -d -p 4000:4000 hawkingrei/tidb-playground:nightly-2025-09-16
+ until mysql -h 127.0.0.1 -P 4000 -u root --connect-timeout=3 -e "SELECT 1" 2>/dev/null; do sleep 3; done
+ - name: Create SQLancer user
+ run: mysql -h 127.0.0.1 -P 4000 -u root -D test -e "CREATE USER 'sqlancer'@'%' IDENTIFIED WITH mysql_native_password BY 'sqlancer'; GRANT ALL PRIVILEGES ON *.* TO 'sqlancer'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;"
+ - name: Run Tests
+ run: |
+ TIDB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestTiDBTLP test
+ TIDB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestTiDBCERT test
+
+ tidb-qpg:
+ name: QPG Tests (TiDB)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Set up TiDB
+ run: |
+ docker pull hawkingrei/tidb-playground:nightly-2025-09-16
+ docker run --name tidb-server -d -p 4000:4000 hawkingrei/tidb-playground:nightly-2025-09-16
+ until mysql -h 127.0.0.1 -P 4000 -u root --connect-timeout=3 -e "SELECT 1" 2>/dev/null; do sleep 3; done
+ - name: Create SQLancer user
+ run: mysql -h 127.0.0.1 -P 4000 -u root -D test -e "CREATE USER 'sqlancer'@'%' IDENTIFIED WITH mysql_native_password BY 'sqlancer'; GRANT ALL PRIVILEGES ON *.* TO 'sqlancer'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;"
+ - name: Run Tests
+ run: TIDB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestTiDBQPG test
+
+ yugabyte:
+ name: DBMS Tests (YugabyteDB)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Set up Yugabyte
+ run: |
+ docker pull yugabytedb/yugabyte:latest
+ docker run -d --name yugabyte -p7000:7000 -p9000:9000 -p5433:5433 -p9042:9042 yugabytedb/yugabyte:latest bin/yugabyted start --daemon=false --tserver_flags="ysql_yb_enable_listen_notify=true" --master_flags="ysql_yb_enable_listen_notify=true"
+ until pg_isready -h localhost -p 5433; do sleep 1; done
+ until nc -z localhost 9042; do sleep 1; done
+ - name: Run Tests
+ run: |
+ YUGABYTE_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestYSQLNoREC test
+ YUGABYTE_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestYSQLTLP test
+ YUGABYTE_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestYSQLPQS test
+ YUGABYTE_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestYCQL test
+
+ doris:
+ name: DBMS Tests (Apache Doris)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+ - name: install mysql client
+ run: |
+ sudo apt update
+ sudo apt install mysql-client --assume-yes
+ - name: Cache Apache Doris tarball
+ uses: actions/cache@v4
+ with:
+ path: apache-doris-2.1.4-bin-x64.tar.gz
+ key: apache-doris-2.1.4-bin-x64-tarball
+ - name: Set up Apache Doris
+ run: |
+ sudo sysctl -w vm.max_map_count=2000000
+ [ -f apache-doris-2.1.4-bin-x64.tar.gz ] || wget -q https://apache-doris-releases.oss-accelerate.aliyuncs.com/apache-doris-2.1.4-bin-x64.tar.gz
+ tar zxf apache-doris-2.1.4-bin-x64.tar.gz
+ mv apache-doris-2.1.4-bin-x64 apache-doris
+ sudo swapoff -a
+ cd apache-doris/fe
+ ./bin/start_fe.sh --daemon
+ cd ../be
+ ./bin/start_be.sh --daemon
+
+ until mysql -u root -h 127.0.0.1 --port 9030 --connect-timeout=3 -e "SELECT 1" 2>/dev/null; do sleep 3; done
+ IP=$(hostname -I | awk '{print $1}')
+ mysql -u root -h 127.0.0.1 --port 9030 -e "ALTER SYSTEM ADD BACKEND '${IP}:9050';"
+ mysql -u root -h 127.0.0.1 --port 9030 -e "CREATE USER 'sqlancer' IDENTIFIED BY 'sqlancer'; GRANT ALL ON *.* TO sqlancer;"
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Run Tests
+ run: |
+ DORIS_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestDorisNoREC test
+ DORIS_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestDorisPQS test
+ DORIS_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestDorisTLP test
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..6194c1529
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,45 @@
+name: Publish package to the Maven Central Repository and Docker Hub
+on:
+ release:
+ types: [created]
+ workflow_dispatch:
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Maven Central Repository
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ server-id: ossrh
+ server-username: MAVEN_USERNAME
+ server-password: MAVEN_PASSWORD
+ - name: Install gpg secret key
+ run: cat <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") | gpg --batch --import
+ - name: Publish package
+ run: mvn --batch-mode deploy -DskipTests=true -DreleaseBuild=true -Dgpg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}
+ env:
+ MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
+ MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
+ push_to_registry:
+ name: Push Docker image to Docker Hub
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ - name: Build SQLancer
+ run: mvn -B package -DskipTests=true
+ - name: Push to Docker Hub
+ uses: docker/build-push-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+ repository: mrigger/sqlancer
+ tag_with_ref: true
diff --git a/.gitignore b/.gitignore
index 9fd02264e..d7cbeb55f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,16 @@
target/
.classpath
-.settings
+.settings/
+.vscode
.project
.checkstyle
*.DS_Store
+.idea
+SQLancer.iml
+dependency-reduced-pom.xml
+database0.db
+databaseconnectiontest.db
+database*.log
+database*.properties
+database*.script
+databases/
\ No newline at end of file
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 000000000..0a659c1a7
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,114 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.codeComplete.visibilityCheck=enabled
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.APILeak=warning
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
+org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
+org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 47378bb04..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,97 +0,0 @@
-dist: bionic
-language: java
-
-script:
-- cd src && python check_names.py && cd ..
-
-cache:
- directories:
- - target/lib
-after_success:
- - bash <(curl -s https://codecov.io/bash)
-after_failure:
- - cat target/pmd.xml
-branches:
- only:
- - master
-
-matrix:
- include:
- - name: MariaDB
- jdk : oraclejdk11
- before_install:
- - sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xF1656F24C74CD1D8
- - sudo add-apt-repository 'deb [arch=amd64,arm64,ppc64el] http://ftp.utexas.edu/mariadb/repo/10.3/ubuntu bionic main'
- - sudo apt update
- - sudo apt install mariadb-server
- - sudo mysql -e "CREATE USER 'sqlancer'@'localhost' IDENTIFIED BY 'sqlancer'; GRANT ALL PRIVILEGES ON * . * TO 'sqlancer'@'localhost';"
- - MARIADB_AVAILABLE=true mvn -Dtest=TestMariaDB test
- - name : MySQL
- jdk : oraclejdk11
- script:
- - sudo apt-get update && sudo apt-get install libssl-dev libmecab2 libjson-perl mecab-ipadic-utf8
- - sudo apt-get remove mysql-* && wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-server_8.0.20-1ubuntu18.04_amd64.deb-bundle.tar && tar -xvf mysql-server_8.0.20-1ubuntu18.04_amd64.deb-bundle.tar && yes | sudo dpkg -i *.deb
- - sudo mysql -e "CREATE USER 'sqlancer'@'localhost' IDENTIFIED BY 'sqlancer'; GRANT ALL PRIVILEGES ON * . * TO 'sqlancer'@'localhost';"
- - MYSQL_AVAILABLE=true mvn -Dtest=TestMySQL test
- - name: CockroachDB
- jdk : oraclejdk11
- before_install:
- - wget -qO- https://binaries.cockroachdb.com/cockroach-v20.1.2.linux-amd64.tgz | tar xvz
- - cd cockroach-v20.1.2.linux-amd64/ && ./cockroach start-single-node --insecure &
- - sleep 15
- - cd cockroach-v20.1.2.linux-amd64/ && ./cockroach sql --insecure -e "CREATE USER sqlancer; GRANT admin to sqlancer" && cd ..
- script:
- - COCKROACHDB_AVAILABLE=true mvn -Dtest=TestCockroachDB test
- - name: TiDB
- jdk : oraclejdk11
- services:
- - docker
- before_install:
- - docker pull pingcap/tidb:latest
- - docker run --name tidb-server -d -p 4000:4000 pingcap/tidb:latest
- - sleep 15
- - sudo mysql -h 127.0.0.1 -P 4000 -u root -D test -e "CREATE USER 'sqlancer'@'%' IDENTIFIED WITH mysql_native_password BY 'sqlancer'; GRANT ALL PRIVILEGES ON *.* TO 'sqlancer'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;"
- script:
- - TIDB_AVAILABLE=true mvn -Dtest=TestTiDB test
- - name: SQLite3
- jdk : oraclejdk11
- script:
- - mvn -Dtest=TestSQLite3 test
- - name: DuckDB
- jdk : oraclejdk11
- script:
- - mvn -Dtest=TestDuckDB test
- - name: DuckDB (Java 8)
- jdk : openjdk8
- script:
- - mvn -Dtest=TestDuckDB test
- - name: DuckDB (java 13)
- jdk : openjdk13
- script:
- - mvn -Dtest=TestDuckDB test
- - name: Misc
- jdk : oraclejdk11
- script:
- - mvn '-Dtest=!sqlancer.dbms.**' test
- - name: PostgreSQL
- jdk : oraclejdk11
- before_install:
- - sudo apt-get update
- - sudo apt-get --yes remove postgresql\*
- - sudo apt-get install -y postgresql-12 postgresql-client-12
- - sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/12/main/postgresql.conf
- - sudo cp /etc/postgresql/{10,12}/main/pg_hba.conf
- - sudo service postgresql restart 12
- addons:
- postgresql: "12.3"
- env:
- global:
- - PGPORT=5432
- services:
- - postgresql
- before_script:
- - sudo apt-get install locales
- - psql -c "CREATE ROLE sqlancer SUPERUSER LOGIN CREATEDB PASSWORD 'sqlancer';" -U postgres
- - createdb test -U postgres
- script:
- - POSTGRES_AVAILABLE=true mvn -Dtest=TestPostgres test
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..76b8833ea
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,149 @@
+# Development
+
+## Working with Eclipse [[Video Guide]](https://www.youtube.com/watch?v=KsuGrOLKb9Q)
+
+Developing SQLancer using Eclipse is expected to work well. You can import SQLancer with a single step:
+
+```
+File -> Import -> Existing Maven Projects -> Select the SQLancer directory as root directory -> Finish
+```
+If you do not find an option to import Maven projects, you might need to install the [M2Eclipse plugin](https://www.eclipse.org/m2e/).
+
+
+## Implementing Support for a New DBMS
+
+The DuckDB implementation provides a good template for a new implementation. The `DuckDBProvider` class is the central class that manages the creation of the databases and executes the selected test oracles. Try to copy its structure for the new DBMS that you want to implement, and start by generate databases (without implementing a test oracle). As part of this, you will also need to implement the equivalent of `DuckDBSchema`, which represents the database schema of the generated database. After you can successfully generate databases, the next step is to generate one of the test oracles. For example, you might want to implement NoREC (see enum value `NOREC` in `DuckDBOracleFactory`). As part of this, you must also implement a random expression generator (see `DuckDBExpressionGenerator`) and a visitor to derive the textual representation of an expression (see `DuckDBToStringVisitor`).
+
+Please consider the following suggestions when creating a PR to contribute a new DBMS:
+* Ensure that `mvn verify -DskipTests=true` does not result in style violations.
+* Add a [CI test](https://github.com/sqlancer/sqlancer/blob/master/.github/workflows/main.yml) to ensure that future changes to SQLancer are unlikely to break the newly-supported DBMS. It is reasonable to do this in a follow-up PR—please indicate whether you plan to do so in the PR description.
+* Add the DBMS' name to the [check_names.py](https://github.com/sqlancer/sqlancer/blob/master/src/check_names.py) script, which ensures adherence to a common prefix in the Java classes.
+* Add the DBMS' name to the [README.md](https://github.com/sqlancer/sqlancer/blob/master/README.md#supported-dbms) file.
+* It would be easier to review multiple smaller PRs, than one PR that contains the complete implementation. Consider contributing parts of your implementation as you work on their implementation.
+
+### Expected Errors
+
+Most statements have an [ExpectedError](https://github.com/sqlancer/sqlancer/blob/aa0c0eccba4eefa75bfd518f608c9222c692c11d/src/sqlancer/common/query/ExpectedErrors.java) object associated with them. This object essentially contains a list of errors, one of which the database system might return if it cannot successfully execute the statement. These errors are typically added through a trial-and-error process while considering various tradeoffs. For example, consider the [DuckDBInsertGenerator](https://github.com/sqlancer/sqlancer/blob/aa0c0eccba4eefa75bfd518f608c9222c692c11d/src/sqlancer/duckdb/gen/DuckDBInsertGenerator.java#L38) class, whose expected errors are specified in [DuckDBErrors](https://github.com/sqlancer/sqlancer/blob/aa0c0eccba4eefa75bfd518f608c9222c692c11d/src/sqlancer/duckdb/DuckDBErrors.java#L90). When implementing such a generator, the list of expected errors might first be empty. When running the generator for the first time, you might receive an error such as "create unique index, table contains duplicate data", indicating that creating the index failed due to duplicate data. In principle, this error could be avoided by first checking whether the column contains any duplicate values. However, checking this would be expensive and error-prone (e.g., consider string similarity, which might depend on collations); thus, the obvious choice would be to add this string to the list of expected errors, and run the generator again to check for any other expected errors. In other cases, errors might be best addressed through improvements in the generators. For example, it is typically straightforward to generate syntactically-valid statements, which is why syntax errors should not be ignored. This approach is effective in uncovering internal errors; rather than ignoring them as an expected error, report them, and see [Unfixed Bugs](#unfixed-bugs) below.
+
+### Bailing Out While Generating a Statement
+
+In some cases, it might be undesirable or even impossible to generate a specific statement type. For example, consider that SQLancer tries to execute a `DROP TABLE` statement (e.g., see [TiDBDropTableGenerator](https://github.com/sqlancer/sqlancer/blob/30948f34acc2354d6be18a70bdeeebff1e73fa48/src/sqlancer/tidb/gen/TiDBDropTableGenerator.java)), but the database contains only a single table. Dropping the table would result in all subsequent attempts to insert data or query it to fail. Thus, in such a case, it might be more efficient to "bail out" by abandoning the current attempt to generate the statement. This can be achieved by throwing a `IgnoreMeException`. Unlike for other exceptions, SQLancer silently continues execution rather than reporting this exception to the user.
+
+
+### Typed vs. Untyped Expression Generation
+
+Each DBMS implementation provides an expression generator used, for example, to generate expressions used in `WHERE` clauses. We found that DBMS can be roughly classified into "permissive" ones, which apply implicit type conversions when needed and "strict" ones, which provide only few implicit conversions and output an error when the type is unexpected. For example, consider the following test case:
+
+```sql
+CREATE TABLE t0(c0 TEXT);
+INSERT INTO t0 VALUES ('1');
+SELECT * FROM t0 WHERE c0;
+```
+
+If the test case is executed using MySQL, which is a permissive DBMS, the `SELECT` fetches a single row, since the content of the `c0` value is interpreted as a boolean. If the test case is executed using PostgreSQL, which is a strict DBMS, the `SELECT` is not accepted as a valid query, and PostgreSQL outputs an error `"argument of WHERE must be type boolean"`. The implementation of the expression generator depends on whether we are dealing with a permissive or a strict DBMS. Since SQLancer's main goal is to find logic bugs, we want to generate as many valid queries as possible.
+
+For a permissive DBMS, implementing the expression generator is easier, since the expression generator does not need to care about the type of the expression, since the DBMS will apply any necessary conversions implicitly. For MySQL, the main `generateExpression` method thus does not accept any type as an argument (see [MySQLExpressionGenerator](https://github.com/sqlancer/sqlancer/blob/86647df8aa2dd8d167b5c3ce3297290f5b0b2bcd/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java#L54)). This method can be called when a expression is required for, for example, a `WHERE` clause. In principle, this approach can also be used for strict DBMS, by adding errors such as `argument of WHERE must be type boolean` to the list of expected errors. However, using such an "untyped" expression generator for a strict DBMS will result in many semantically invalid queries being generated.
+
+For a strict DBMS, the better approach is typically to attempt to generate expressions of the expected type. For PostgreSQL, the expression generator thus expects an additional type argument (see [PostgreSQLExpressionGenerator](https://github.com/sqlancer/sqlancer/blob/86647df8aa2dd8d167b5c3ce3297290f5b0b2bcd/src/sqlancer/postgres/gen/PostgresExpressionGenerator.java#L251)). This type is propagated recursively. For example, if we require a predicate for the `WHERE` clause, we pass boolean as a type. The expression generator then calls a method `generateBooleanExpression` that attempts to produce a boolean expression, by, for example, generating a comparison (e.g., `<=`). For the comparison's operands, a random type is then selected and propagated. For example, if an integer type is selected, then `generateExpression` is called with this type once for the left operand, and once for the right operand. Note that this process does not guarantee that the expression will indeed have the expected type. It might happen, for example, that the expression generator attempts to produce an integer value, but that it produces a double value instead, namely when an integer overflow occurs, which, depending on the DBMS, implicitly converts the result to a floating-point value.
+
+#### Supported DBMS
+
+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. |
+| 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. |
+
+#### Previously Supported DBMS
+
+Some DBMS were once supported but subsequently removed.
+
+| DBMS | Pull Request | Description |
+| ---------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| ArangoDB | [#915](https://github.com/sqlancer/sqlancer/pull/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](https://github.com/sqlancer/sqlancer/pull/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](https://github.com/sqlancer/sqlancer/pull/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](https://github.com/sqlancer/sqlancer/pull/963) | This implementation was removed because development of StoneDB stopped.
+| CnosDB | | This implementation was removed because the CnosDB image is unstable under SQLancer's DDL load (see [cnosdb/cnosdb#2435](https://github.com/cnosdb/cnosdb/issues/2435)) and the project appears no longer maintained. |
+
+### Unfixed Bugs
+
+Often, some bugs are fixed only after an extended period, meaning that SQLancer will repeatedly report the same bug. In such cases, it might be possible to avoid generating the problematic pattern, or adding an expected error with the internal error message. Rather than, for example, commenting out the code with the bug-inducing pattern, a pattern implemented by the [TiDBBugs class](https://github.com/sqlancer/sqlancer/blob/4c20a94b3ad2c037e1a66c0b637184f8c20faa7e/src/sqlancer/tidb/TiDBBugs.java) should be applied. The core idea is to use a public, static flag for each issue, which is set to true as long as the issue persists (e.g., see [bug35652](https://github.com/sqlancer/sqlancer/blob/4c20a94b3ad2c037e1a66c0b637184f8c20faa7e/src/sqlancer/tidb/TiDBBugs.java#L55)). The work-around code is then executed—or the problematic pattern should not be generated—if the flag is set to true (e.g., [an expected error is added for bug35652](https://github.com/sqlancer/sqlancer/blob/59564d818d991d54b32fa5a79c9f733799c090f2/src/sqlancer/tidb/TiDBErrors.java#L47)). This makes it easy to later on identify and remove all such work-around code once the issue has been fixed.
+
+## Options
+
+SQLancer uses [JCommander](https://jcommander.org/) for handling options. The `MainOptions` class contains options that are expected to be supported by all DBMS-testing implementations. Furthermore, each `*Provider` class provides a method to return an additional set of supported options.
+
+An option can include lowercase alphanumeric characters, and hyphens. The format of the options is checked by a unit test.
+
+## Continuous Integration and Test Suite
+
+To improve and maintain SQLancer's code quality, we use multiple tools:
+* The [Eclipse code formatter](https://code.revelc.net/formatter-maven-plugin/), to ensure a consistent formatting (Run `mvn formatter:format` to format all files).
+* [Checkstyle](https://checkstyle.sourceforge.io/), to enforce a consistent coding standard.
+* [PMD](https://pmd.github.io/), which finds programming flaws using static analysis.
+* [SpotBugs](https://spotbugs.github.io/), which also uses static analysis to find bugs and programming flaws.
+
+You can run them using the following command:
+
+```
+mvn verify
+```
+
+We use [GitHub Actions](https://github.com/sqlancer/sqlancer/blob/master/.github/workflows/main.yml) to automatically check PRs.
+
+
+## Testing
+
+As part of the GitHub Actions check, we use smoke testing by running SQLancer on each supported DBMS for some minutes, to test that nothing is obviously broken. For DBMS for which all bugs have been fixed, we verify that SQLancer cannot find any further bugs (i.e., the return code is zero).
+
+In addition, we use [unit tests](https://github.com/sqlancer/sqlancer/tree/master/test/sqlancer) to test SQLancer's core functionality, such as random string and number generation as well as option passing. When fixing a bug, add a unit test, if it is easily possible.
+
+You can run the tests using the following command:
+
+```
+mvn test
+```
+
+Note that per default, the smoke testing is performed only for embedded DBMS (e.g., DuckDB and SQLite). To run smoke tests also for the other DBMS, you need to set environment variables. For example, you can run the MySQL smoke testing (and no other tests) using the following command:
+
+```
+MYSQL_AVAILABLE=true mvn -Dtest=TestMySQL test
+```
+
+For up-to-date testing commands, check out the `.github/workflows/main.yml` file.
+
+## Reviewing
+
+Reviewing is an effective way of improving code quality. Everyone is welcome to review any PRs. Currently, all PRs are reviewed at least by the main contributor, @mrigger. Contributions by @mrigger are currently not (necessarily) reviewed, which is not ideal. If you are willing to regularly and timely review PRs, indicate so in the SQLancer Slack workspace.
+
+## Naming Conventions
+
+Each class specific to a DBMS is prefixed by the DBMS name. For example, each class specific to SQLite is prefixed by `SQLite3`. The naming convention is [automatically checked](src/check_names.py).
+
+## Commit History
+
+Please pay attention to good commit messages (in particular subject lines). As basic guidelines, we recommend a blog post on [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) written Chris Beams, which provides 7 useful rules. Implement at least the following of those rules:
+1. Capitalize the subject line. For example, write "**R**efactor the handling of indexes" rather than "**r**efactor the handling of indexes".
+2. Do not end the subject line with a period. For example, write "Refactor the handling of indexes" rather than "Refactor the handling of indexes.".
+3. Use the imperative mood in the subject line. For example, write "Refactor the handling of indexes" rather than "Refactoring" or "Refactor**ed** the handling of indexes".
+
+Please also pay attention to a clean commit history. Rather than merging with the main branch, use `git rebase` to rebase your commits on the main branch. Sometimes, it might happen that you discover an issue only after having already created a commit, for example, when an issue is found by `mvn verify` in the CI checks. Do not introduce a separate commit for such issues. If the issue was introduced by the last commit, you can fix the issue, and use `git commit --amend` to change the latest commit. If the change was introduced by one of the previous commits, you can use `git rebase -i` to change the respective commit. If you already have a number of such commits, you can use `git squash` to "collapse" multiple commits into one. For more information, you might want to read [How (and Why!) to Keep Your Git Commit History Clean](https://about.gitlab.com/blog/2018/06/07/keeping-git-commit-history-clean/) written by Kushal Pandya.
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..54f3aacaf
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM ubuntu:21.04
+
+RUN apt-get update --yes && env DEBIAN_FRONTEND=noninteractive apt-get install openjdk-15-jdk maven --yes --no-install-recommends
+
+# assumes that the project has already been built
+COPY target/sqlancer-*.jar sqlancer.jar
+COPY target/lib/*.jar /lib/
+
+ENTRYPOINT ["java", "-jar", "sqlancer.jar"]
diff --git a/README.md b/README.md
index 56ec5cf43..f41e32d3c 100644
--- a/README.md
+++ b/README.md
@@ -1,102 +1,116 @@
-
-[](https://codecov.io/gh/sqlancer/sqlancer)
-[](https://twitter.com/sqlancer_dbms)
-# SQLancer
+[](https://github.com/sqlancer/sqlancer/actions)

-SQLancer (Synthesized Query Lancer) is a tool to automatically test Database Management Systems (DBMS) in order to find logic bugs in their implementation. We refer to logic bugs as those bugs that cause the DBMS to fetch an incorrect result set (e.g., by omitting a record).
+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 operates in the following two phases:
+SQLancer tackles two essential challenges when automatically testing the DBMSs:
+1. **Test input generation**: SQLancer implements approaches for automatically generating SQL statements. It contains various hand-written SQL generators that operate in multiple phases. First, a database schema is created, which refers to a set of tables and their columns. Then, data is inserted into these tables, along with creating various other kinds of database states such as indexes, views, or database-specific options. Finally, queries are generated, which can be validated using one of multiple result validators (also called *test oracles*) that SQLancer provides. Besides the standard approach of creating the statements in an unguided way, SQLancer also supports a test input-generation approach that is feedback-guided and aims to exercise as many unique query plans as possible based on the intuition that doing so would exercise many interesting behaviors in the database system [[ICSE '23]](https://arxiv.org/pdf/2312.17510).
+2. **Test oracles**: A key innovation in SQLancer is that it provides ways to find deep kinds of bugs in DBMSs. As a main focus, it can find logic bugs, which are bugs that cause the DBMS to fetch an incorrect result set (e.g., by omitting a record). We have proposed multiple complementary test oracles such as *Ternary Logic Partitioning (TLP)* [[OOPSLA '20]](https://dl.acm.org/doi/pdf/10.1145/3428279), *Non-optimizing Reference Engine Construction (NoREC)* [[ESEC/FSE 2020]](https://arxiv.org/abs/2007.08292), *Pivoted Query Synthesis (PQS)* [[OSDI '20]](https://www.usenix.org/system/files/osdi20-rigger.pdf), *Differential Query Plans (DQP)* [[SIGMOD '24]](https://dl.acm.org/doi/pdf/10.1145/3654991), and *Constant Optimization Driven Database System Testing (CODDTest)* [SIGMOD '25]. It can also find specific categories of performance issues, which refer to cases where a DBMS could reasonably be expected to produce its result more efficiently using a technique called *Cardinality Estimation Restriction Testing (CERT)* [[ICSE '24]](https://arxiv.org/pdf/2306.00355). SQLancer can detect unexpected internal errors (e.g., an error that the database is corrupted) by declaring all potential errors that might be returned by a DBMS for a given query. Finally, SQLancer can find crash bugs, which are bugs that cause the DBMS process to terminate. For this, it uses an implicit test oracle.
-1. Database generation: The goal of this phase is to create a populated database, and stress the DBMS to increase the probability of causing an inconsistent database state that could be detected subsequently. First, random tables are created. Then, randomly SQL statements are chosen to generate, modify, and delete data. Also other statements, such as those to create indexes as well as views and to set DBMS-specific options are sent to the DBMS.
-2. Testing: The goal of this phase is to detect the logic bugs based on the generated database. See Testing Approaches below.
+**Community.** We have a [Slack workspace](https://join.slack.com/t/sqlancer/shared_invite/zt-eozrcao4-ieG29w1LNaBDMF7OB_~ACg) to discuss SQLancer, and DBMS testing in general. Previously, SQLancer had an account on Twitter/X [@sqlancer_dbms](https://twitter.com/sqlancer_dbms), which is no longer maintained. We have a [blog](https://sqlancer.github.io/posts/), which, as of now, contains only posts by contributors of the [Google Summer of Code project](https://summerofcode.withgoogle.com/archive/2023/organizations/sqlancer).
-# Getting Started
+# Getting Started [[Video Guide]](https://www.youtube.com/watch?v=lcZ6LixPH1Y)
-Requirements:
-* Java 8 or above
-* [Maven](https://maven.apache.org/) (`sudo apt install maven` on Ubuntu)
-* The DBMS that you want to test (SQLite is an embedded DBMS and is included)
+Minimum Requirements:
+* Java 11 or above
+* [Maven](https://maven.apache.org/)
-The following commands clone SQLancer, create a JAR, and start SQLancer to fuzz SQLite using Ternary Logic Query Partitioning (TLP):
+The following commands clone SQLancer, create a JAR, and start SQLancer to test SQLite using [Non-optimizing Reference Engine Construction (NoREC)](https://arxiv.org/abs/2007.08292):
```
git clone https://github.com/sqlancer/sqlancer
cd sqlancer
mvn package -DskipTests
cd target
-java -jar SQLancer-0.0.1-SNAPSHOT.jar --num-threads 4 sqlite3 --oracle NoREC
+java -jar sqlancer-*.jar --num-threads 4 sqlite3 --oracle NoREC
```
-If the execution prints progress information every five seconds, then the tool works as expected. Note that SQLancer might find bugs in SQLite. Before reporting these, be sure to check that they can still be reproduced when using the latest development version. 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.
+**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.
-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 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.
+**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.
-# Potential Commercialization
+**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](https://github.com/sqlancer/sqlancer/blob/main/pom.xml). Note that any crashes in these systems will also cause a crash in the JVM on which SQLancer runs.
-Due to the significant interest that we have received, we are considering to commercialize our bug-finding efforts. If you represent a company and would be interested in a bug-finding service, please approach us ([Manuel Rigger](mailto:manuel.rigger@inf.ethz.ch) and [Zhendong Su](mailto:zhendong.su@inf.ethz.ch)) with your expectations and requirements for such a service.
-# Research Prototype
-
-This project should at this stage still be seen as a research prototype. We believe that the tool is not ready to be used. However, we have received many requests by companies, organizations, and individual developers, which is why we decided to prematurely release the tool. Expect errors, incompatibilities, lack of documentation, and insufficient code quality. That being said, we are working hard to address these issues and enhance SQLancer to become a production-quality piece of software. We welcome any issue reports, extension requests, and code contributions.
-
-# Testing Approaches
-
-| 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](https://arxiv.org/abs/2001.04174). 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](https://www.manuelrigger.at/preprints/NoREC.pdf). 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. |
+# Using SQLancer
-Please find the `.bib` entries [here](docs/DEVELOPMENT.md).
+**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.
-# Supported DBMS
+**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](https://www.debuggingbook.org/html/DeltaDebugger.html). 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](https://embed.cs.utah.edu/creduce/), which requires specifying the test oracle in a script that can be executed by C-Reduce.
-Since SQL dialects differ widely, each DBMS to be tested requires a separate implementation.
+**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.
-| 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 | |
-| 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. |
+**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:
+* [GitHub](https://github.com/sqlancer/sqlancer/releases)
+* [Maven Central](https://search.maven.org/artifact/com.sqlancer/sqlancer)
+* [DockerHub](https://hub.docker.com/r/mrigger/sqlancer)
-# Using SQLancer
+**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](https://github.com/sqlancer/sqlancer/blob/documentation/.github/workflows/main.yml). 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.
-## Logs
+**Supported DBMSs.** SQLancer requires DBMS-specific code for each DBMS that it supports. As of January 2025, it provides support for Citus, ClickHouse, CockroachDB, Databend, (Apache) DataFusion, (Apache) Doris, DuckDB, H2, HSQLDB, MariaDB, Materialize, MySQL, OceanBase, PostgreSQL, Presto, QuestDB, SQLite3, TiDB, and YugabyteDB. The extent to which the individual DBMSs are supported [differs](https://github.com/sqlancer/sqlancer/blob/documentation-approaches/CONTRIBUTING.md).
-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.
+# Approaches and Papers
-## Reducing a Bug
+SQLancer has pioneered and includes multiple approaches for DBMS testing, as outlined below in chronological order.
-After finding a bug, it is useful to produce a minimal test case before reporting the bug, to save the DBMS developers' time and effort. For many test cases, [C-Reduce](https://embed.cs.utah.edu/creduce/) does a great job. In addition, we have been working on a SQL-specific reducer, which we plan to release soon.
+| Technique | Venue | Links | Description |
+|-----------------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Pivoted Query Synthesis (PQS) | OSDI 2020 | [Paper](https://www.usenix.org/system/files/osdi20-rigger.pdf) [Video](https://www.youtube.com/watch?v=0aeDyXgzo04 ) | 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 effectively detects bugs, but requires more implementation effort than other testing approaches that follow a metamorphic testing or differential testing methodology. Thus, it is currently unmaintained. |
+| Non-optimizing Reference Engine Construction (NoREC) | ESEC/FSE 2020 | [Paper](https://arxiv.org/abs/2007.08292) [Video](https://www.youtube.com/watch?v=4mbzytrWJhQ) | NoREC aims to find optimization bugs. 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. The approach applies primarily to simple queries with a filter predicate. |
+| Ternary Logic Partitioning (TLP) | OOPSLA 2020 | [Paper](https://dl.acm.org/doi/pdf/10.1145/3428279) [Video](https://www.youtube.com/watch?v=FN9OLbGh0VI) | TLP partitions a query into three partitioning queries, whose results are composed and compared 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. It is among the most widely adopted testing techniques. |
+| Differential Query Execution (DQE) | ICSE 2023 | [Paper](https://ieeexplore.ieee.org/document/10172736) [Code](https://github.com/sqlancer/sqlancer/pull/1251) | Differential Query Execution (DQE) is a novel and general approach to detect logic bugs in SELECT, UPDATE and DELETE queries. DQE solves the test oracle problem by executing SELECT, UPDATE and DELETE queries with the same predicate φ, and observing inconsistencies among their execution results. For example, if a row that is updated by an UPDATE query with a predicate φ does not appear in the query result of a SELECT query with the same predicate φ, a logic bug is detected in the target DBMS. We append two extra columns to each table in a database to uniquely identify each row and track whether a row has been modified. We further rewrite SELECT and UPDATE queries to identify their accessed rows. DQE supports MySQL. |
+| Query Plan Guidance (QPG) | ICSE 2023 | [Paper](https://arxiv.org/pdf/2312.17510) [Video](https://youtu.be/6EjQ1cKiZJU?si=gh7uoykRqNjl3GXR&t=1820) [Code](https://github.com/sqlancer/sqlancer/issues/641) | QPG is a feedback-guided test case generation approach. It is based on the insights that query plans capture whether interesting behavior is exercised within the DBMS. It works by mutating the database state when no new query plans have been observed after executing a number of queries, expecting that the new state enables new query plans to be triggered. This approach is enabled by option `--qpg-enable` and supports TLP and NoREC oracles for SQLite, CockroachDB, TiDB, and Materialize. It is the only approach that specifically tackles the test input generation problem. |
+| Cardinality Estimation Restriction Testing (CERT) | ICSE 2024 | [Paper](https://arxiv.org/pdf/2306.00355) [Code](https://github.com/sqlancer/sqlancer/issues/822) | CERT aims to find performance issues through unexpected estimated cardinalities, which represent the estimated number of returned rows. From a given input query, it derives a more restrictive query, whose estimated cardinality should be no more than that of the original query. A violation indicates a potential performance issue. CERT supports TiDB, CockroachDB, and MySQL. CERT is the only test oracle that is part of SQLancer that was designed to find performance issues. |
+| Differential Query Plans (DQP) | SIGMOD 2024 | [Paper](https://dl.acm.org/doi/pdf/10.1145/3654991) [Video](https://www.youtube.com/watch?v=9Qp7quJfGEk) [Code](https://github.com/sqlancer/sqlancer/issues/918) | DQP aims to find logic bugs by controlling the execution of different query plans for a given query and validating that they produce a consistent result. DQP supports MySQL, MariaDB, and TiDB. |
+| Constant Optimization Driven Database System Testing (CODDTest) | SIGMOD 2025 | [Code](https://github.com/sqlancer/sqlancer/pull/1054) | CODDTest finds logic bugs in DBMSs, including in advanced features such as subqueries. It is based on the insight that we can assume the database state to be constant for a database session, which then enables us to substitute parts of a query with their results, essentially corresponding to constant folding and constant propagation, which are two traditional compiler optimizations. |
-## Found Bugs
+Please find the `.bib` entries [here](docs/PAPERS.md). |
-We would appreciate it if you mention SQLancer when you report bugs found by it. We would also be excited to know if you are using SQLancer to find bugs, or if you have extended it to test another DBMS (also if you do not plan to contribute it to this project). SQLancer has found over 400 bugs in widely-used DBMS, which are listed [here](https://www.manuelrigger.at/dbms-bugs/).
+# FAQ
+**I am running SQLancer on the latest version of a supported DBMS. Is it expected that SQLancer prints many AssertionErrors?** In many cases, SQLancer does not support the latest version of a DBMS. You can check the [`.github/workflows/main.yml`](https://github.com/sqlancer/sqlancer/blob/master/.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](https://github.com/sqlancer/sqlancer/blob/354d591cfcd37fa1de85ec77ec933d5d975e947a/src/sqlancer/common/query/ExpectedErrors.java) so that SQLancer ignores them when they appear (e.g., this is useful if some error-inducing patterns cannot easily be avoided).
-# Community
+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](https://www.postgresql.org/docs/current/locale.html)).
-We have created a [Slack workspace](https://join.slack.com/t/sqlancer/shared_invite/zt-eozrcao4-ieG29w1LNaBDMF7OB_~ACg) to discuss SQLancer, and DBMS testing in general. SQLancer's official Twitter handle is [@sqlancer_dbms](https://twitter.com/sqlancer_dbms).
+**When starting SQLancer, I get an error such as "database 'test' does not exist". How can I run SQLancer without this error?** 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](https://www.postgresql.org/docs/current/sql-createdatabase.html)).
+# Links
-# Additional Documentation
+Documentation and resources:
-* [Contributing to SQLancer](docs/DEVELOPMENT.md)
+* [Contributing to SQLancer](CONTRIBUTING.md)
* [Papers and .bib entries](docs/PAPERS.md)
+* More information on our DBMS testing efforts and the bugs we found is available [here](https://www.manuelrigger.at/dbms-bugs/).
-# Additional Resources
+Videos:
+* [SQLancer Tutorial Playlist](https://www.youtube.com/playlist?list=PLm7ofmclym1E2LwBeSer_AAhzBSxBYDci)
+* [SQLancer Talks](https://youtube.com/playlist?list=PLm7ofmclym1E9-AbYy-PkrMfHpB9VdlZJ)
-* A talk on Ternary Logic Partitioning (TLP) and SQLancer is available on [YouTube](https://www.youtube.com/watch?v=Np46NQ6lqP8).
-* An (older) Pivoted Query Synthesis (PQS) talk is available on [YouTube](https://www.youtube.com/watch?v=yzENTaWe7qg).
-* PingCAP has implemented PQS, NoREC, and TLP in a tool called [go-sqlancer](https://github.com/chaos-mesh/go-sqlancer).
-* More information on our DBMS testing efforts and the bugs we found is available [here](https://www.manuelrigger.at/dbms-bugs/).
+Closely related tools:
+* [go-sqlancer](https://github.com/chaos-mesh/go-sqlancer): re-implementation of some of SQLancer's approaches in Go by PingCAP
+* [Jepsen](https://github.com/jepsen-io): testing of distributed (database) systems
+* [SQLRight](https://github.com/PSU-Security-Universe/sqlright): coverage-guided DBMS fuzzer, also supporting NoREC and TLP
+* [SQLsmith](https://github.com/anse1/sqlsmith): random SQL query generator used for fuzzing
+* [Squirrel](https://github.com/s3team/Squirrel): coverage-guided DBMS fuzzer
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index ba6bb1342..000000000
--- a/codecov.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-codecov:
- require_ci_to_pass: no
- notify:
- after_n_builds: 10
-
-coverage:
- range: "50...100"
-
-coverage:
- status:
- project:
- default:
- threshold: 2%
- patch:
- default:
- threshold: 2%
diff --git a/configs/checkstyle.xml b/configs/checkstyle.xml
index 10d21514e..530bd41d9 100644
--- a/configs/checkstyle.xml
+++ b/configs/checkstyle.xml
@@ -80,9 +80,13 @@
-
-
+
+
+
+
+
@@ -169,9 +173,16 @@
-
+
+
+
+
+
+
+
+
diff --git a/configs/pmd-rules.xml b/configs/pmd-rules.xml
index 8098884d4..656e29f85 100644
--- a/configs/pmd-rules.xml
+++ b/configs/pmd-rules.xml
@@ -17,12 +17,13 @@
-
+
+ 2
@@ -51,6 +52,7 @@
2
+
@@ -70,13 +72,13 @@
+ 2
-
@@ -85,5 +87,11 @@
-
+
+ 2
+
+
+
+
+
diff --git a/configs/spotbugs-exclude.xml b/configs/spotbugs-exclude.xml
index 1b146bf10..7fa4de560 100644
--- a/configs/spotbugs-exclude.xml
+++ b/configs/spotbugs-exclude.xml
@@ -7,4 +7,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/Citus README.md b/docs/Citus README.md
new file mode 100644
index 000000000..7bd3ffb49
--- /dev/null
+++ b/docs/Citus README.md
@@ -0,0 +1,89 @@
+# SQLancer for Citus (PostgreSQL extension)
+
+SQLancer (Synthesized Query Lancer) is a tool to automatically test Database Management Systems (DBMS) in order to find logic bugs in their implementation. More information about the tool can be found in the [SQLancer README](https://github.com/sqlancer/sqlancer).
+
+The Citus implementation of SQLancer supports the Ternary Logic Query Partitioning (TLP) test oracle.
+
+# Setting up
+
+Instructions for setting up SQLancer are described in [SQLancer - Getting Started](https://github.com/sqlancer/sqlancer#getting-started).
+
+Requirements for Citus:
+* PostgreSQL & Citus - The steps required to build Citus from source are described in [Contributing to Citus](https://github.com/citusdata/citus/blob/master/CONTRIBUTING.md).
+Optional Tools for Citus:
+* [pgenv](https://github.com/thanodnl/pgenv) (for easier management of PostgreSQL versions)
+* [citus_dev](https://github.com/citusdata/tools/tree/develop/citus_dev) (for easier configuration of Citus environment)
+
+# Using SQLancer
+
+The following commands run the Citus implementation of SQLancer using Ternary Logic Query Partitioning (TLP):
+
+```
+cd target
+java -jar SQLancer-0.0.1-SNAPSHOT.jar --num-threads 4 citus --oracle QUERY_PARTITIONING
+```
+
+How to configure the run and how to find the output logs is explained in [SQLancer - Using SQLancer](https://github.com/sqlancer/sqlancer#using-sqlancer).
+
+The `--repartition` flag is a boolean optional argument specific to the Citus implementation (and therefore should be used after `citus` on the command line) that enables [repartition joins](https://docs.citusdata.com/en/v9.3/develop/api_guc.html?highlight=repartition%20join#citus-enable-repartitioned-insert-select-boolean). It is set to `true` by default.
+
+## Interpreting output logs
+
+### Current logs
+
+If the `--log-each-select` option is enabled, each database being tested has a corresponding `-cur.log` file that is populated with all SQL statements sent to the database.
+
+### Error logs
+
+When a bug is found in a database being tested, a corresponding `.log` file is created and is populated with all SQL statements necessary to reproduce the bug.
+
+1. At the top of the file is the (commented-out) error message, which provides information about the panic error/logic bug detected.
+2. Below that are (commented-out) lines that give more information about the specific thread being run, including the seed value (which can be passed in as a command line flag in a later run to reproduce the same thread run).
+3. Then, the steps to create the Citus database cluster are provided as commented-out lines. (Following these steps are equivalent to running `citus_dev make XXX` or following the [Citus Docs instructions](https://docs.citusdata.com/en/v9.3/installation/single_machine_debian.html) for setting up a single-machine cluster.)
+4. The rest of the file (not commented-out) contains the SQL statements that prepare the testing database.
+5. If the bug detected is a logic bug (the error was raised by the TLP Oracle), then the pair of buggy SELECT statements whose result sets mismatch are also appended to the end of the file as commented-out lines.
+
+It is important to note that these `.log` files are valid sources of SQL commands that can be passed in with the `-f` flag to the `psql` command. As long as the empty database that the file is being passed into is created with Citus support and the proper worker nodes as described in step 3, this will reproduce the state that the testing database was in when the error was detected. Then, the SQL statement(s) that caused the error can be executed to reproduce the error itself.
+
+Once a bug is identified, it is also possible to check whether the bug is particular to Citus or was inherited from PostgreSQL, since Citus is a PostgreSQL extension. For this, a copy of the `.log` file can be made where all Citus-specific statements (distributing a table, creating a reference table etc.) are removed. Executing this file on an empty database would produce the “vanilla” state that the database would be in without any Citus functionalities. Then, the SQL statement(s) that caused the error can be executed here to check whether the error is reproduced in “vanilla” PostgreSQL as well.
+
+# Maintaining & Contributing
+
+The instructions for setting up a development environment for contributing to SQLancer are explained in [SQLancer - Development](https://github.com/sqlancer/sqlancer/blob/master/CONTRIBUTING.md).
+
+## Updating expected/ignored Citus errors
+
+The `CitusBugs.java` file in the `src/sqlancer/citus/` directory and the `CitusCommon.java` file in the `src/sqlancer/citus/gen/` directory should be continuously updated to reflect the currently unsupported functionalities and active bugs.
+
+Not all SQL commands generated by SQLancer are supported by the DBMS - they might raise `SQLException`s. For instance, a command that involves an invalid casting may raise a `cannnot cast type` error. These errors do not indicate any bugs in the DBMS, which is why it is desirable to quietly ignore them if raised. The `PostgresCommon` and `CitusCommon` classes in SQLancer collect these expected errors and ensure that SQLancer does not explicitly raise an error if an expected error is thrown.
+
+The `addCitusErrors()` method in `CitusCommon.java` adds Citus-specific errors to the pool of expected errors. It is important to note that it is enough for a string to be a substring of the error message for an error to be ignored. This method is populated with errors that are expected in Citus behavior either because the SQL command generated by SQLancer is currently not supported by Citus, or because a bug that has already been identified has not been fixed yet and is redundantly re-appearing. Both of these, especially the latter group, are dynamic and require updating.
+
+The `CitusBugs` class in `CitusBugs.java` is an interface between [issues](https://github.com/citusdata/citus/issues?q=is%3Aissue+label%3Asqlancer) opened in the Citus GitHub repository and the bugs listed in the `addCitusErrors()` method in `CitusCommon.java`. Each bug is assigned a corresponding boolean variable, which can be switched to `false` (uninitialized) when the error is fixed on the Citus master branch.
+
+### What to do: new bug found
+
+If the bug found is a panic error, i.e. NOT a logic bug (mismatch in result sets identified by the TLP Oracle), this error should be added to the `CitusBugs` class and the `addCitusErrors()` method.
+1. Open an issue for the bug in the [Citus GitHub repository](https://github.com/citusdata/citus/issues?q=is%3Aissue+label%3Asqlancer+), and tag the issue with the `sqlancer` label.
+2. Add a boolean variable associated with this issue to the `CitusBugs` class and set it to `true`.
+3. Add the error message to the `addCitusErrors()` method wrapped inside an if-statement referring to the boolean created in the `CitusBugs` class.
+
+If the bug found is a logic bug, i.e. a mismatch in result sets identified by the TLP Oracle, perform step 1 only.
+
+### What to do: bug fixed
+
+If the bug fixed was a panic error, i.e. NOT a logic bug (mismatch in result sets identified by the TLP Oracle), the boolean in the `CitusBugs` class corresponding to the issue resolved should be set to `false` (uninitialized) once the fix is merged to the Citus master branch.
+
+If the bug found was a logic bug, i.e. a mismatch in result sets identified by the TLP Oracle, no actions are necessary.
+
+### What to do: change in Citus support for PostgreSQL commands
+
+An error that was previously raised by Citus due to unsupported PostgreSQL functionalities can be removed from the `addCitusErrors()` method if Citus begins supporting this functionality.
+
+## Modifying the database environment setup
+
+The `CitusProvider.java` file in the `src/sqlancer/citus/` directory includes the methods for connecting to an existing database and creating the distributed database environment, as well as for preparing the environment for testing (creation of local, distributed, and reference tables and modification of these tables).
+
+## Modifying JOINs in the SELECT statements generated for testing
+
+The `CitusTLPBase.java` file in the `src/sqlancer/citus/oracle/tlp/` directory includes the methods for generating JOIN clauses, which can be modified to alter the scope of the JOINs.
\ No newline at end of file
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
deleted file mode 100644
index c54e44b4f..000000000
--- a/docs/DEVELOPMENT.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Development
-
-## Working with Eclipse
-
-Developing SQLancer using Eclipse is expected to work well. You can import SQLancer with a single step:
-
-```
-File -> Import -> Existing Maven Projects -> Select the SQLancer directory as root directory -> Finish
-```
-If you do not find an option to import Maven projects, you might need to install the [M2Eclipse plugin](https://www.eclipse.org/m2e/).
-
-
-## Implementing Support for a New DBMS
-
-The DuckDB implementation provides a good template for a new implementation. The `DuckDBProvider` class is the central class that manages the creation of the databases and executes the selected test oracles. Try to copy its structure for the new DBMS that you want to implement, and start by generate databases (without implementing a test oracle). As part of this, you will also need to implement the equivalent of `DuckDBSchema`, which represents the database schema of the generated database. After you can successfully generate databases, the next step is to generate one of the test oracles. For example, you might want to implement NoREC (see `DuckDBNoRECOracle` or `DuckDBQueryPartitioningWhereTester` for TLP). As part of this, you must also implement a random expression generator (see `DuckDBExpressionGenerator`) and a visitor to derive the textual representation of an expression (see `DuckDBToStringVisitor`).
-
-## Options
-
-SQLancer uses [JCommander](https://jcommander.org/) for handling options. The `MainOptions` class contains options that are expected to be supported by all DBMS-testing implementations. Furthermore, each `*Provider` class provides a method to return an additional set of supported options.
-
-An option can include lowercase alphanumeric characters, and hyphens. The format of the options is checked by a unit test.
-
-## Continuous Integration and Test Suite
-
-To improve and maintain SQLancer's code quality, we use multiple tools:
-* The [Eclipse code formatter](https://code.revelc.net/formatter-maven-plugin/), to ensure a consistent formatting (Run `mvn formatter:format` to format all files).
-* [Checkstyle](https://checkstyle.sourceforge.io/), to enforce a consistent coding standard.
-* [PMD](https://pmd.github.io/), which finds programming flaws using static analysis.
-* [SpotBugs](https://spotbugs.github.io/), which also uses static analysis to find bugs and programming flaws.
-
-You can run them using the following command:
-
-```
-mvn verify
-```
-
-We use [Travis-CI](https://travis-ci.com/) to automatically check PRs.
-
-
-## Testing
-
-We found that bugs in SQLancer are quickly found and easy to debug when testing the DBMS. However, it would still be preferable to automatically check that SQLancer still executes as expected. To this end, we would like to add smoke testing for each DBMS to test that the respective testing implementation is not obviously broken, see [here](https://github.com/sqlancer/sqlancer/issues/3).
-
-## Naming Conventions
-
-Each class specific to a DBMS is prefixed by the DBMS name. For example, each class specific to SQLite is prefixed by `SQLite3`. The naming convention is [automatically checked](src/check_names.py).
diff --git a/docs/PAPERS.md b/docs/PAPERS.md
index 0ed6390c7..a42b42c12 100644
--- a/docs/PAPERS.md
+++ b/docs/PAPERS.md
@@ -1,6 +1,6 @@
# Papers
-The testing approaches implemented in SQLancer are described in the three papers below.
+The testing approaches implemented in SQLancer are described in the four papers below.
## Testing Database Engines via Pivoted Query Synthesis
@@ -8,16 +8,19 @@ This paper describes PQS, a testing approach to detect various kinds of logic bu
```
@inproceedings{Rigger2020PQS,
- author={Manuel Rigger and Zhendong Su},
- title={Testing Database Engines via Pivoted Query Synthesis},
- year={2020},
- url={https://arxiv.org/abs/2001.04174}
+ title = {Testing Database Engines via Pivoted Query Synthesis},
+ booktitle = {14th {USENIX} Symposium on Operating Systems Design and Implementation ({OSDI} 20)},
+ year = {2020},
+ address = {Banff, Alberta},
+ url = {https://www.usenix.org/conference/osdi20/presentation/rigger},
+ publisher = {{USENIX} Association},
+ month = nov,
}
```
## Detecting Optimization Bugs in Database Engines via Non-Optimizing Reference Engine Construction
-This paper describes NoREC, a metamorphic testing approach to detect optimization bugs, that is, logic bugs that affect the query optimizer. A preprint is available [here](https://www.manuelrigger.at/preprints/NoREC.pdf).
+This paper describes NoREC, a metamorphic testing approach to detect optimization bugs, that is, logic bugs that affect the query optimizer. A preprint is available [here](https://arxiv.org/abs/2007.08292).
```
@inproceedings{Rigger2020NoREC,
@@ -26,18 +29,77 @@ This paper describes NoREC, a metamorphic testing approach to detect optimizatio
booktitle = {Proceedings of the 2020 28th ACM Joint Meeting on European Software Engineering Conference and Symposium on the Foundations of Software Engineering},
series={ESEC/FSE 2020},
location={Sacramento, California, United States},
- year={2020}
+ year={2020},
+ doi={10.1145/3368089.3409710}
}
```
## Ternary Logic Partitioning: Detecting Logic Bugs in Database Management Systems
-This paper describes TLP, a metamorphic testing approach that can detect various kinds of logic bugs and is applicable also test features such as aggregate functions. A preprint is available [here](https://www.manuelrigger.at/preprints/TLP.pdf).
+This paper describes TLP, a metamorphic testing approach that can detect various kinds of logic bugs and is applicable also to test features such as aggregate functions. A preprint is available [here](https://www.manuelrigger.at/preprints/TLP.pdf).
```
-@inproceedings{Rigger2020TLP,
+@article{Rigger2020TLP,
author={Manuel Rigger and Zhendong Su},
- title={Ternary Logic Partitioning: Detecting Logic Bugs in Database Management Systems},
- year={2020}
+ title={Finding Bugs in Database Systems via Query Partitioning},
+ journal = {Proc. ACM Program. Lang.},
+ number = {OOPSLA},
+ year={2020},
+ doi={10.1145/3428279},
+ volume={4},
+ articleno={211}
+}
+```
+
+## Testing Database Engines via Query Plan Guidance
+
+This paper describes Query Plan Guidance (QPG), a test case generation method guided by query plan coverage. This method can be paired with above three testing methods. A preprint is available [here](http://bajinsheng.github.io/assets/pdf/qpg_icse23.pdf).
+
+```
+@inproceedings{Ba2023QPG,
+ author = {Ba, Jinsheng and Rigger, Manuel},
+ title = {Testing Database Engines via Query Plan Guidance},
+ booktitle = {The 45th International Conference on Software Engineering (ICSE'23)},
+ year = {2023},
+ month = may
+}
+```
+
+## CERT: Finding Performance Issues in Database Systems Through the Lens of Cardinality Estimation
+
+This paper describes CERT, a testing approach to find performance issues by inspecting inconsistent estimated cardinalities. A preprint is available [here](https://bajinsheng.github.io/assets/pdf/cert_icse24.pdf).
+
+```
+@inproceedings{cert,
+ author = {Ba, Jinsheng and Rigger, Manuel},
+ title = {CERT: Finding Performance Issues in Database Systems Through the Lens of Cardinality Estimation},
+ booktitle = {The 46th International Conference on Software Engineering (ICSE'24)},
+ year = {2024},
+ month = apr,
+}
+```
+
+## Keep It Simple: Testing Databases via Differential Query Plans
+
+This paper describes DQP, a testing approach to find logic bugs in database systems by comparing the query plans of different database systems. A preprint is available [here](https://bajinsheng.github.io/assets/pdf/dqp_sigmod24.pdf).
+
+```
+@article{dqp,
+ author = {Ba, Jinsheng and Rigger, Manuel},
+ title = {Keep It Simple: Testing Databases via Differential Query Plans},
+ year = {2024},
+ issue_date = {June 2024},
+ publisher = {Association for Computing Machinery},
+ address = {New York, NY, USA},
+ journal = {Proceeding of ACM Management of Data (SIGMOD'24)},
+ month = jun
}
```
+
+# Comparing SQLancer With Other Tools that Find Logic Bugs
+
+If you want to fairly compare other tools with SQLancer, we would be glad to provide feedback (e.g., feel free to send an email to manuel.rigger@inf.ethz.ch). We have the following general recommendations and comments:
+* PostgreSQL and SQLite are DBMSs that we comprehensively tested, and where all or most of the bugs that SQLancer could find were fixed. We believe these two systems to be the most challenging test targets. Finding bugs that the approaches implemented in SQLancer overlooked in these systems might thus best demonstrate a new approach's effectiveness. For some other DBMSs like MySQL and MariaDB, SQLancer could still detect unreported bugs; we stopped testing these DBMSs and reporting bugs due to the large number of unfixed bugs.
+* We programmatically disabled the generation of features that are likely to trigger known bugs (e.g., see [TiDB](https://github.com/sqlancer/sqlancer/blob/master/src/sqlancer/tidb/TiDBBugs.java)). If a comparison investigates metrics such as code coverage that is achieved when fuzzing a DBMS, it might be desirable to enable the generation of such features.
+* For the default SQLite JDBC driver, a number of extensions (e.g., the [soundex function](https://sqlite.org/lang_corefunc.html#soundex)) are disabled by default, which is why they are also disabled by default in the DBMS' options (e.g., see [SQLite3Options](https://github.com/sqlancer/sqlancer/blob/c71b9741f680f4877fc5047445787ed184a5a5e0/src/sqlancer/sqlite3/SQLite3Options.java#L67)). To investigate metrics such as code coverage, it might again be desirable to enable such options.
+* The maximum expression depth (see the `--max-expression-depth` option), the number of queries issued per database (see the `--num-queries` option), and the number of tables and views that are created (currently, SQLancer does not have an option to set these) significantly influence the tool's effectiveness and performance characteristics. It might be desirable to experiment with different values for the expression depth (e.g., values between 2 and 4), the number of queries (1000-100,000), as well as the number of tables and views.
diff --git a/docs/QueryPlanGuidance.md b/docs/QueryPlanGuidance.md
new file mode 100644
index 000000000..bb467461b
--- /dev/null
+++ b/docs/QueryPlanGuidance.md
@@ -0,0 +1,66 @@
+# Query Plan Guidance
+Query Plan Guidance (QPG) is a test case generation method that attempts to explore unseen query plans. 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. Here, we document all mutators in which we choose the most promising one that may help covering more unique query plans to execute.
+
+# Mutators
+All mutators are listed below and implemented in the enumeration variables `Action` in the `XXDBProvider.java` file of each DBMS.
+The `Mutator` column includes the items in the `Action` enumeration variable.
+The `Example` column includes an example of a realistic statement generated by this mutator.
+The `Description` column includes an explanation of what the mutator does.
+The `More unique query plans...` column explains why applying this mutator may help covering more unique query plans.
+
+
+|DBMS |Mutator |Example |Description |More unique query plans may be covered because of |
+|-----------|---------------------|--------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|
+|SQLite |PRAGMA |PRAGMA automatic_index true; |It modifies database options. |different options that decide how to execute statements.|
+|SQLite |CREATE_INDEX |CREATE INDEX i0 ON t0 WHERE c0 ISNULL; |It adds a new index on a table. |subsequent differnt logic of querying data. |
+|SQLite |CREATE_VIEW |CREATE VIEW v0(c0) AS SELECT DISTINCT ABS(t0.c2) FROM t0; |It adds a new view from existing tables. |more possible execution logics on the view. |
+|SQLite |CREATE_TABLE |CREATE TABLE t0 (c0 INT CHECK ((c0) BETWEEN (1) AND (10)) ); |It adds a new table. |more possible execution logics on the table. |
+|SQLite |CREATE_VIRTUALTABLE |CREATE VIRTUAL TABLE vt1 USING fts5(c0 UNINDEXED); |It adds a new table with fts5 feature. |more possible execution logics on the table with fts5. |
+|SQLite |CREATE_RTREETABLE |CREATE VIRTUAL TABLE rt0 USING rtree_i32(c0, c1, c2, c3, c4); |It adds a new table with rtree feature. |more possible execution logics on the table with rtree. |
+|SQLite |INSERT |INSERT INTO t0(c0, c1) VALUES ('lrd+a*', NULL); |It inserts a new row to a table. |subsequent different logic of querying data. |
+|SQLite |DELETE |DELETE FROM t0 WHERE (c0>3); |It deletes specific rows from a table. |subsequent different logic of querying data. |
+|SQLite |ALTER |ALTER TABLE t0 ADD COLUMN c39 REAL; |It changes the schema of a table. |more possible execution logics on the changed table. |
+|SQLite |UPDATE |UPDATE t0 SET (c2, c0)=(-944, 'L((xA') WHERE t0.c1; |It updates specific data of a table. |subsequent different logic of querying data. |
+|SQLite |DROP_INDEX |DROP INDEX i0; |It drops an index. |subsequent different logic of querying data. |
+|SQLite |DROP_TABLE |DROP TABLE t0; |it drops an table. |subsequent different logic of querying data. |
+|SQLite |DROP_VIEW |DROP VIEW v0; |It drops a view. |subsequent different logic of querying data. |
+|SQLite |VACUUM |VACUUM main; |It rebuilds the database file. |subsequent different logic of querying data. |
+|SQLite |REINDEX |REINDEX t0; |It drops and recreates indexes from scratch. |subsequent different logic of querying data. |
+|SQLite |ANALYZE |ANALYZE t0; |It gathers statistics about tables to help make better query planning choices.|subsequent different logic of querying data. |
+|SQLite |EXPLAIN |EXPLAIN SELECT * FROM t0; |It obtains query plan of a query. |subsequent different logic of querying data. |
+|SQLite |CHECK_RTREE_TABLE |SELECT rtreecheck('rt0'); |It runs an integrity check on a table. |subsequent different logic of querying data. |
+|SQLite |VIRTUAL_TABLE_ACTION |INSERT INTO vt0(vt0) VALUES('rebuild'); |It changes the options of a virtual table. |subsequent different logic of querying data. |
+|SQLite |MANIPULATE_STAT_TABLE|INSERT INTO sqlite_stat1 VALUES('rt0', 't1', '2'); |It changes the table that stores statistics of all tables. |subsequent different logic of querying data. |
+|SQLite |TRANSACTION_START |BEGIN TRANSACTION; |All statements after this will not be committed. |subsequent different logic of querying data. |
+|SQLite |ROLLBACK_TRANSACTION |ROLLBACK TRANSACTION; |All statements after last BEGIN are dropped. |subsequent different logic of querying data. |
+|SQLite |COMMIT |COMMIT; |All statements after last BEGIN are committed |subsequent different logic of querying data. |
+|TiDB |CREATE_TABLE |CREATE TABLE t1(c0 INT); |It adds a new table. |more possible execution logics on the table. |
+|TiDB |CREATE_INDEX |CREATE INDEX i0 ON t0(c0(250) ASC) KEY_BLOCK_SIZE 1564693810209727437; |It adds a new index on a table. |subsequent differnt logic of querying data. |
+|TiDB |VIEW_GENERATOR |CREATE VIEW v0(c0, c1) AS SELECT t1.c0, ((t1.c0)REGEXP('8')) FROM t1; |It adds a new view from existing tables. |more possible execution logics on the view. |
+|TiDB |INSERT |INSERT INTO t0(c0) VALUES (-16387); |It inserts a new row to a table. |subsequent different logic of querying data. |
+|TiDB |ALTER_TABLE |ALTER TABLE t1 ADD PRIMARY KEY(c0); |It changes the schema of a table. |more possible execution logics on the changed table. |
+|TiDB |TRUNCATE |TRUNCATE t0; |It drops all rows of a table. |subsequent different logic of querying data. |
+|TiDB |UPDATE |UPDATE t0 SET c0='S' WHERE t0.c0; |It updates specific data of a table. |subsequent different logic of querying data. |
+|TiDB |DELETE |DELETE FROM t0 ORDER BY CAST(t0.c0 AS CHAR) DESC; |It deletes specific rows from a table. |subsequent different logic of querying data. |
+|TiDB |SET |set @@tidb_max_chunk_size=8864; |It modifies database options. |different options that decide how to execute statements.|
+|TiDB |ADMIN_CHECKSUM_TABLE |ADMIN CHECKSUM TABLE t0; |it calculate the checksum for a table. |subsequent different logic of querying data. |
+|TiDB |ANALYZE_TABLE |ANALYZE TABLE t1 WITH 174 BUCKETS; |It gathers statistics about tables to help make better query planning choices.|subsequent different logic of querying data. |
+|TiDB |DROP_TABLE |DROP TABLE t0; |it drops an table. |subsequent different logic of querying data. |
+|TiDB |DROP_VIEW |DROP VIEW v0; |It drops a view. |subsequent different logic of querying data. |
+|CockroachDB|CREATE_TABLE |CREATE TABLE t1 (c0 INT4, c1 VARBIT(44) UNIQUE DEFAULT (B'000'), CONSTRAINT "primary" PRIMARY KEY(c1 ASC, c0 ASC));|It adds a new table. |more possible execution logics on the table. |
+|CockroachDB|CREATE_INDEX |CREATE INDEX ON t0(rowid); |It adds a new index on a table. |subsequent differnt logic of querying data. |
+|CockroachDB|CREATE_VIEW |CREATE VIEW v0(c0) AS SELECT DISTINCT MIN(TIMETZ '1970-01-11T12:19:44') FROM t0; |It adds a new view from existing tables. |more possible execution logics on the view. |
+|CockroachDB|CREATE_STATISTICS |CREATE STATISTICS s0 FROM t2; |It gathers statistics about tables to help make better query planning choices.|subsequent different logic of querying data. |
+|CockroachDB|INSERT |INSERT INTO t1 (rowid, c0) VALUES(NULL, true) ON CONFLICT (c0) DO NOTHING ; |It inserts a new row to a table. |subsequent different logic of querying data. |
+|CockroachDB|UPDATE |UPDATE t0@{FORCE_INDEX=t0_pkey} SET c0=t0.c0; |It updates specific data of a table. |subsequent different logic of querying data. |
+|CockroachDB|SET_SESSION |SET SESSION BYTEA_OUTPUT=escape; |It changes session configurations. |different options that decide how to execute statements.|
+|CockroachDB|SET_CLUSTER_SETTING |SET CLUSTER SETTING sql.query_cache.enabled=true; |It changes cluster configurations. |different options that decide how to execute statements.|
+|CockroachDB|DELETE |DELETE from t0; |It deletes specific rows from a table. |subsequent different logic of querying data. |
+|CockroachDB|TRUNCATE |TRUNCATE TABLE t1 CASCADE; |It drops all rows of a table. |subsequent different logic of querying data. |
+|CockroachDB|DROP_TABLE |DROP TABLE t0; |it drops an table. |subsequent different logic of querying data. |
+|CockroachDB|DROP_VIEW |DROP VIEW v0; |It drops a view. |subsequent different logic of querying data. |
+|CockroachDB|COMMENT_ON |COMMENT ON INDEX t0_c0_key IS '|?'; |It changes schema of a table. |subsequent different logic of querying data. |
+|CockroachDB|SHOW |SHOW LOCALITY; |It lists detailed information of active queries. |subsequent different logic of querying data. |
+|CockroachDB|EXPLAIN |EXPLAIN SELECT * FROM t0; |It obtains query plan of a query. |subsequent different logic of querying data. |
+|CockroachDB|SCRUB |EXPERIMENTAL SCRUB table t0; |It checks data corruption of a table. |subsequent different logic of querying data. |
+|CockroachDB|SPLIT |ALTER INDEX t0@t0_c0_key SPLIT AT VALUES (NULL); |It changes the indexes. |subsequent different logic of querying data. |
diff --git a/docs/testCaseReduction.md b/docs/testCaseReduction.md
new file mode 100644
index 000000000..ee317f791
--- /dev/null
+++ b/docs/testCaseReduction.md
@@ -0,0 +1,50 @@
+# Test Case Reduction
+SQLancer generates a large number of statements, but not all of them are relevant to the bug. To automatically reduce the test cases, two reducers were implemented: the statement reducer and the AST-based reducer.
+
+## Statement Reducer
+The statement reducer utilizes the delta-debugging technique to remove irrelevant statements. More details of delta-debugging could be found in this paper: [Simplifying and Isolating Failure-Inducing Input](https://www.cs.purdue.edu/homes/xyzhang/fall07/Papers/delta-debugging.pdf).
+
+Using the statement reducer, SQLancer reduces the set of statements to a minimal subset that reproduces the bug.
+
+## AST-Based Reducer
+The AST-based reducer can shorten a statement by applying AST level transformations, including removing unnecessary clauses, irrelevant elements in a list, simplify complicated expressions and etc.
+
+The transformations are implemented by [JSQLParser](https://github.com/JSQLParser/JSqlParser), a RDBMS agnostic SQL statement parser that can translate SQL statements into a traversable hierarchy of Java classes. JSQLParser provides support for the SQL standard as well as major SQL dialects. The AST-based reducer works for any SQL dialects that can be parsed by this tool.
+
+## Implementing reproducer
+Determining whether a bug persists after reducing statements
+is an undecidable task for general transformations.
+In practice, reducers use the [reproducer](../src/sqlancer/Reproducer.java) to determine
+if a bug remains after statements have been removed or modified.
+The reducer's responsibility is to verify if the current state,
+formed by the pared-down statements,
+continues to yield incorrect results for specific queries.
+
+Different oracles have distinct logic for determination,
+meaning a universal reproducer doesn't exist.
+Each oracle type needs its own reproducer implementation.
+If reproducer is not implemented for specific oracle,
+test case reduction is not available while using the oracle.
+
+Oracles for which reproducers have currently been implemented include:
+1. for [`SQLite3NoRECOracle`](../src/sqlancer/sqlite3/oracle/SQLite3NoRECOracle.java)
+2. for [`TiDBTLPWhereOracle`](../src/sqlancer/tidb/oracle/TiDBTLPWhereOracle.java)
+
+## Using reducers
+Test-case reduction is disabled by default. The statement reducer can be enabled by passing `--use-reducer` when starting SQLancer. If you wish to further shorten each statements, you need to additionally pass the `--reduce-ast` parameter so that the AST-based reduction is applied.
+
+Note: if `--reduce-ast` is set, `--use-reducer` option must be enabled first.
+
+There are also options to define timeout seconds and max steps of reduction for both statement reducer and AST-based reducer.
+
+```
+--statement-reducer-max-steps=
+--statement-reducer-max-time=
+--ast-reducer-max-steps=
+--ast-reducer-max-time=
+```
+
+## Reduction logs
+If test-case reduction is enabled, each time the reducer performs a reduction step successfully,it prints the reduced statements to the log file, overwriting the previous ones.
+
+The log files will be stored in the following format: `logs//reduce/-reduce.log`. For instance, if the tested DBMS is SQLite3 and the current database is named database0, the log file will be located at `logs/sqlite3/reduce/database0-reduce.log`.
diff --git a/pom.xml b/pom.xml
index 8669c0664..c4bc71f82 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,9 +2,38 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- SQLancer
- SQLancer
- 0.0.1-SNAPSHOT
+ com.sqlancer
+ sqlancer
+ 2.0.0
+ SQLancer
+ http://www.sqlancer.com/
+ SQLancer finds logic bugs in Database Management Systems through automatic testing
+
+
+ MIT License
+ https://github.com/sqlancer/sqlancer/blob/master/LICENSE.md
+ repo
+
+
+
+
+ mrigger
+ Manuel Rigger
+ manuel.rigger@inf.ethz.ch
+ ETH Zurich
+ https://ethz.ch/
+ https://www.manuelrigger.at/
+
+ architect
+ developer
+
+
+
+
+ https://github.com/sqlancer/sqlancer/
+ scm:git:git://github.com/sqlancer/sqlancer.git
+ scm:git:ssh://github.com:sqlancer/sqlancer.git
+ UTF-8
@@ -12,6 +41,46 @@
srctest
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.4.0
+
+
+ package
+
+ shade
+
+
+
+
+ com.beust:jcommander
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ package
+
+ run
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.pluginsmaven-surefire-plugin
@@ -20,7 +89,7 @@
org.jacocojacoco-maven-plugin
- 0.8.5
+ 0.8.12
@@ -37,11 +106,15 @@
+ org.apache.maven.pluginsmaven-compiler-plugin
- 3.8.1
+ 3.10.1
- 8
- 8
+ 11
+ 11
+
+ ${project.basedir}/.settings/org.eclipse.jdt.core.prefs
+ eclipsetruetrue
@@ -50,19 +123,24 @@
org.codehaus.plexusplexus-compiler-eclipse
- 2.8.6
+ 2.13.0org.eclipse.jdtecj
- 3.22.0
+ 3.28.0
+
+
+ org.codehaus.plexus
+ plexus-compiler-api
+ 2.13.0net.revelc.code.formatterformatter-maven-plugin
- 2.12.0
+ 2.20.0eclipseformat
@@ -76,6 +154,7 @@
org.apache.maven.pluginsmaven-dependency-plugin
+ 3.4.0copy-dependencies
@@ -88,6 +167,7 @@
falsefalsetrue
+ jcommander
@@ -95,8 +175,9 @@
org.apache.maven.pluginsmaven-jar-plugin
- 3.2.0
+ 3.3.0
+ truetrue
@@ -123,7 +204,14 @@
org.apache.maven.pluginsmaven-checkstyle-plugin
- 3.1.1
+ 3.2.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.5.0
+
+ configs/checkstyle.xml
@@ -143,7 +231,7 @@
org.apache.maven.pluginsmaven-pmd-plugin
- 3.13.0
+ 3.14.0pmd
@@ -164,7 +252,7 @@
com.github.spotbugsspotbugs-maven-plugin
- 4.0.4
+ 4.7.3.0spotbugs
@@ -183,51 +271,165 @@
+
+ com.google.auto.service
+ auto-service
+ 1.0.1
+ com.beustjcommander
- 1.78
+ 1.82org.postgresqlpostgresql
- 42.2.14
+ 42.5.1
+
+
+ com.ing.data
+ cassandra-jdbc-wrapper
+ 4.7.0
+
+
+ com.yugabyte
+ jdbc-yugabytedb
+ 42.3.5-yb-1org.xerialsqlite-jdbc
- 3.32.3
+ 3.49.1.0
- mysql
- mysql-connector-java
- 8.0.20
+ com.mysql
+ mysql-connector-j
+ 9.7.0org.mariadb.jdbcmariadb-java-client
- 2.6.1
+ 3.1.0org.duckdbduckdb_jdbc
- 0.1.9
+ 1.3.0.0
+
+
+ com.facebook.presto
+ presto-jdbc
+ 0.283org.junit.jupiterjunit-jupiter-engine
- 5.6.2
+ 5.11.2testorg.slf4j
- slf4j-simple
- 1.7.30
+ slf4j-simple
+ 2.0.6ru.yandex.clickhouseclickhouse-jdbc
- 0.2.4
+ 0.3.2
+
+
+ com.h2database
+ h2
+ 2.3.232
+
+
+ org.mongodb
+ mongodb-driver-sync
+ 4.1.1
+
+
+ com.arangodb
+ arangodb-java-driver
+ 6.9.0
+
+
+ org.questdb
+ questdb
+ 6.5.3
+
+
+ org.hsqldb
+ hsqldb
+ 2.7.4
+ runtime
+
+
+ org.apache.commons
+ commons-csv
+ 1.9.0
+
+
+ com.github.jsqlparser
+ jsqlparser
+ 4.6
+
+
+ org.apache.arrow
+ flight-sql-jdbc-driver
+ 16.1.0
+
+
+ org.apache.hive
+ hive-jdbc
+ 3.1.2
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+
+
+
+
+ org.apache.hive
+ hive-serde
+ 4.0.1
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+
+
+
+
+ org.apache.hive
+ hive-cli
+ 4.0.1
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.24.3
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.24.3
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ 2.24.3
+
+
+ org.apache.hadoop
+ hadoop-common
+ 3.2.4
@@ -235,8 +437,115 @@
org.apache.maven.pluginsmaven-jxr-plugin
- 3.0.0
+ 3.3.0
+
+
+ ossrh
+ Central Repository OSSRH
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+ jdk-8-config
+
+ [1.3,1.9)
+
+
+ ${java.home}/../bin/javadoc
+
+
+
+ jdk-11-config
+
+ [11,)
+
+
+ ${java.home}/bin/javadoc
+
+
+
+ release-steps
+
+
+ releaseBuild
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.4.1
+
+ 8
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.0.1
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+
+
+ datafusion-tests
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.3.0
+
+
+ **/TestDataFusion.java
+
+ --add-opens java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED
+
+
+
+
+
+
diff --git a/src/check_names.py b/src/check_names.py
index f76b881ab..453580f88 100644
--- a/src/check_names.py
+++ b/src/check_names.py
@@ -1,28 +1,55 @@
import os
+import sys
+from typing import List
-def get_java_files(directory):
- java_files = []
- for root, dirs, files in os.walk(directory):
- for f in files:
- if f.endswith('.java'):
- java_files.append(f)
- return java_files
-
-def verify_prefix(prefix, files):
- if len(files) == 0:
- print(prefix + ' directory does not contain any files!')
- exit(-1)
- for f in files:
- if not f.startswith(prefix):
- print('The class name of ' + f + ' does not start with ' + prefix)
- exit(-1)
-
-# TODO: ClickHouse (wait for https://github.com/sqlancer/sqlancer/pull/39)
-verify_prefix('CockroachDB', get_java_files("sqlancer/cockroachdb/"))
-verify_prefix('DuckDB', get_java_files("sqlancer/duckdb"))
-verify_prefix('MariaDB', get_java_files("sqlancer/mariadb/"))
-verify_prefix('MySQL', get_java_files("sqlancer/mysql/"))
-verify_prefix('Postgres', get_java_files("sqlancer/postgres/"))
-verify_prefix('SQLite3', get_java_files("sqlancer/sqlite3/"))
-verify_prefix('TiDB', get_java_files("sqlancer/tidb/"))
+def get_java_files(directory_path: str) -> List[str]:
+ java_files: List[str] = []
+ for root, dirs, files in os.walk(directory_path):
+ for f in files:
+ if f.endswith('.java'):
+ java_files.append(f)
+ return java_files
+
+
+def verify_one_db(prefix: str, files: List[str]):
+ print('checking database, name: {0}, files: {1}'.format(prefix, files))
+ if len(files) == 0:
+ print(prefix + ' directory does not contain any files!', file=sys.stderr)
+ exit(-1)
+ for f in files:
+ if not f.startswith(prefix):
+ print('The class name of ' + f + ' does not start with ' + prefix, file=sys.stderr)
+ exit(-1)
+ print('checking database pass: ', prefix)
+
+
+def verify_all_dbs(name_to_files: dict[str:List[str]]):
+ for db_name, files in name_to_files.items():
+ verify_one_db(db_name, files)
+
+
+if __name__ == '__main__':
+ cwd = os.getcwd()
+ print("Current working directory: {0}".format(cwd))
+ name_to_files: dict[str:List[str]] = dict()
+ name_to_files["Citus"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "citus"))
+ name_to_files["ClickHouse"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "clickhouse"))
+ name_to_files["CockroachDB"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "cockroachdb"))
+ name_to_files["Databend"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "databend"))
+ name_to_files["DataFusion"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "datafusion"))
+ name_to_files["DuckDB"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "duckdb"))
+ name_to_files["H2"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "h2"))
+ name_to_files["HSQLDB"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "hsqldb"))
+ name_to_files["MariaDB"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "mariadb"))
+ name_to_files["Materialize"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "materialize"))
+ name_to_files["MySQL"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "mysql"))
+ name_to_files["OceanBase"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "oceanbase"))
+ name_to_files["Postgres"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "postgres"))
+ name_to_files["Presto"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "presto"))
+ name_to_files["QuestDB"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "questdb"))
+ name_to_files["SQLite3"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "sqlite3"))
+ name_to_files["TiDB"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "tidb"))
+ name_to_files["Y"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "yugabyte")) # has both YCQL and YSQL prefixes
+ name_to_files["Doris"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "doris"))
+ verify_all_dbs(name_to_files)
diff --git a/src/sqlancer/ASTBasedReducer.java b/src/sqlancer/ASTBasedReducer.java
new file mode 100644
index 000000000..f9468af76
--- /dev/null
+++ b/src/sqlancer/ASTBasedReducer.java
@@ -0,0 +1,144 @@
+package sqlancer;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import sqlancer.common.query.Query;
+import sqlancer.common.query.SQLQueryAdapter;
+import sqlancer.transformations.RemoveClausesOfSelect;
+import sqlancer.transformations.RemoveColumnsOfSelect;
+import sqlancer.transformations.RemoveElementsOfExpressionList;
+import sqlancer.transformations.RemoveRowsOfInsert;
+import sqlancer.transformations.RemoveUnions;
+import sqlancer.transformations.RoundDoubleConstant;
+import sqlancer.transformations.SimplifyConstant;
+import sqlancer.transformations.SimplifyExpressions;
+import sqlancer.transformations.Transformation;
+
+public class ASTBasedReducer, O extends DBMSSpecificOptions>, C extends SQLancerDBConnection>
+ implements Reducer {
+
+ private final DatabaseProvider provider;
+
+ @SuppressWarnings("unused")
+ private G state;
+ private G newGlobalState;
+ private Reproducer reproducer;
+
+ private List> reducedStatements;
+ // statement after reduction.
+
+ public ASTBasedReducer(DatabaseProvider provider) {
+ this.provider = provider;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void updateStatements(String queryString, int index) {
+ boolean couldAffectSchema = queryString.contains("CREATE TABLE") || queryString.contains("EXPLAIN");
+ reducedStatements.set(index, (Query) new SQLQueryAdapter(queryString, couldAffectSchema));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void reduce(G state, Reproducer reproducer, G newGlobalState) throws Exception {
+ this.state = state;
+ this.newGlobalState = newGlobalState;
+ this.reproducer = reproducer;
+
+ long maxReduceTime = state.getOptions().getMaxStatementReduceTime();
+ long maxReduceSteps = state.getOptions().getMaxStatementReduceSteps();
+
+ List> initialBugInducingStatements = state.getState().getStatements();
+ newGlobalState.getState().setStatements(new ArrayList<>(initialBugInducingStatements));
+
+ List transformations = new ArrayList<>();
+
+ transformations.add(new RemoveUnions());
+ transformations.add(new RemoveClausesOfSelect());
+ transformations.add(new RemoveRowsOfInsert());
+ transformations.add(new RemoveColumnsOfSelect());
+ transformations.add(new RemoveElementsOfExpressionList());
+ transformations.add(new SimplifyExpressions());
+ transformations.add(new SimplifyConstant());
+ transformations.add(new RoundDoubleConstant());
+
+ Transformation.setBugJudgement(() -> {
+ try {
+ return this.bugStillTriggers();
+ } catch (Exception ignored) {
+ }
+ return false;
+ });
+
+ boolean observeChange;
+ reducedStatements = new ArrayList<>();
+ for (Query> query : initialBugInducingStatements) {
+ reducedStatements.add((Query) query);
+ }
+
+ Instant startTime = Instant.now();
+ reduceProcess: do {
+ observeChange = false;
+ for (Transformation t : transformations) {
+ for (int i = 0; i < reducedStatements.size(); i++) {
+
+ Instant currentTime = Instant.now();
+ if (maxReduceTime != MainOptions.NO_REDUCE_LIMIT
+ && Duration.between(startTime, currentTime).getSeconds() >= maxReduceTime) {
+ break reduceProcess;
+ }
+
+ if (maxReduceSteps != MainOptions.NO_REDUCE_LIMIT
+ && Transformation.getReduceSteps() >= maxReduceSteps) {
+ break reduceProcess;
+ }
+
+ Query> query = reducedStatements.get(i);
+ boolean initFlag = t.init(query.getQueryString());
+ int index = i;
+ t.setStatementChangedCallBack((statementString) -> {
+ updateStatements(statementString, index);
+ });
+
+ if (!initFlag) {
+ System.out.println("Error when parsing the statement at transformer :" + t);
+ continue;
+ }
+ t.apply();
+ observeChange |= t.changed();
+ }
+ }
+ } while (observeChange);
+
+ newGlobalState.getState().setStatements(new ArrayList<>(reducedStatements));
+ newGlobalState.getLogger().logReduced(newGlobalState.getState(),
+ "AST-based reduction finished; the following statements remain");
+ }
+
+ public boolean bugStillTriggers() throws Exception {
+ try (C con2 = provider.createDatabase(newGlobalState)) {
+ newGlobalState.setConnection(con2);
+ List> candidateStatements = new ArrayList<>(reducedStatements);
+ newGlobalState.getState().setStatements(new ArrayList<>(candidateStatements));
+
+ for (Query s : candidateStatements) {
+ try {
+ s.execute(newGlobalState);
+ } catch (Throwable ignoredException) {
+ // ignore
+ }
+ }
+ try {
+ if (reproducer.bugStillTriggers(newGlobalState)) {
+ newGlobalState.getLogger().logReduced(newGlobalState.getState());
+ return true;
+ }
+ } catch (Throwable ignoredException) {
+
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/sqlancer/AbstractAction.java b/src/sqlancer/AbstractAction.java
index d2181e041..db218ebe8 100644
--- a/src/sqlancer/AbstractAction.java
+++ b/src/sqlancer/AbstractAction.java
@@ -1,9 +1,19 @@
package sqlancer;
-import java.sql.SQLException;
+import sqlancer.common.query.Query;
public interface AbstractAction {
- Query getQuery(G globalState) throws SQLException;
+ Query> getQuery(G globalState) throws Exception;
+
+ /**
+ * Specifies whether it makes sense to request a {@link Query}, when the previous call to {@link #getQuery(Object)}
+ * returned a query that failed executing.
+ *
+ * @return whether retrying getting queries makes sense, if the first query failed executing.
+ */
+ default boolean canBeRetried() {
+ return true;
+ }
}
diff --git a/src/sqlancer/ComparatorHelper.java b/src/sqlancer/ComparatorHelper.java
index 45d3b5d74..cee290924 100644
--- a/src/sqlancer/ComparatorHelper.java
+++ b/src/sqlancer/ComparatorHelper.java
@@ -1,14 +1,18 @@
package sqlancer;
import java.io.IOException;
-import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
+import sqlancer.common.query.ExpectedErrors;
+import sqlancer.common.query.SQLQueryAdapter;
+import sqlancer.common.query.SQLancerResultSet;
+
public final class ComparatorHelper {
private ComparatorHelper() {
@@ -29,11 +33,11 @@ static boolean equals(double a, double b) {
return true;
}
// If the difference is less than epsilon, treat as equal.
- return Math.abs(a - b) < 0.0001 * Math.max(Math.abs(a), Math.abs(b));
+ return Math.abs(a - b) < 0.001 * Math.max(Math.abs(a), Math.abs(b)) + 0.001;
}
- public static List getResultSetFirstColumnAsString(String queryString, Set errors,
- GlobalState> state) throws SQLException {
+ public static List getResultSetFirstColumnAsString(String queryString, ExpectedErrors errors,
+ SQLGlobalState, ?> state) throws SQLException {
if (state.getOptions().logEachSelect()) {
// TODO: refactor me
state.getLogger().writeCurrent(queryString);
@@ -44,38 +48,38 @@ public static List getResultSetFirstColumnAsString(String queryString, S
e.printStackTrace();
}
}
- QueryAdapter q = new QueryAdapter(queryString, errors);
+ boolean canonicalizeString = state.getOptions().canonicalizeSqlString();
+ SQLQueryAdapter q = new SQLQueryAdapter(queryString, errors, true, canonicalizeString);
List resultSet = new ArrayList<>();
- ResultSet result = null;
+ SQLancerResultSet result = null;
try {
result = q.executeAndGet(state);
if (result == null) {
throw new IgnoreMeException();
}
while (result.next()) {
- resultSet.add(result.getString(1));
+ String resultTemp = result.getString(1);
+ if (resultTemp != null) {
+ resultTemp = resultTemp.replaceAll("[\\.]0+$", ""); // Remove the trailing zeros as many DBMS treat
+ // it as non-bugs
+ }
+ resultSet.add(resultTemp);
}
- result.getStatement().close();
} catch (Exception e) {
if (e instanceof IgnoreMeException) {
throw e;
}
- if (e instanceof NumberFormatException) {
- // https://github.com/tidb-challenge-program/bug-hunting-issue/issues/57
- throw new IgnoreMeException();
- }
- if (e.getMessage() == null) {
- throw new AssertionError(queryString, e);
- }
- for (String error : errors) {
- if (e.getMessage().contains(error)) {
+
+ Throwable current = e;
+ while (current != null) {
+ if (current.getMessage() != null && errors.errorIsExpected(current.getMessage())) {
throw new IgnoreMeException();
}
+ current = current.getCause();
}
throw new AssertionError(queryString, e);
} finally {
if (result != null && !result.isClosed()) {
- result.getStatement().close();
result.close();
}
}
@@ -83,42 +87,63 @@ public static List getResultSetFirstColumnAsString(String queryString, S
}
public static void assumeResultSetsAreEqual(List resultSet, List secondResultSet,
- String originalQueryString, List combinedString, GlobalState> state) {
+ String originalQueryString, List combinedString, SQLGlobalState, ?> state) {
if (resultSet.size() != secondResultSet.size()) {
- String queryFormatString = "%s; -- cardinality: %d";
+ String queryFormatString = "-- %s;" + System.lineSeparator() + "-- cardinality: %d"
+ + System.lineSeparator();
String firstQueryString = String.format(queryFormatString, originalQueryString, resultSet.size());
- String secondQueryString = String.format(queryFormatString,
- combinedString.stream().collect(Collectors.joining(";")), secondResultSet.size());
- state.getState().statements.add(new QueryAdapter(firstQueryString));
- state.getState().statements.add(new QueryAdapter(secondQueryString));
- String assertionMessage = String.format("the size of the result sets mismatch (%d and %d)!\n%s\n%s",
- resultSet.size(), secondResultSet.size(), firstQueryString, secondQueryString);
+ String combinedQueryString = String.join(";", combinedString);
+ String secondQueryString = String.format(queryFormatString, combinedQueryString, secondResultSet.size());
+ state.getState().getLocalState()
+ .log(String.format("%s" + System.lineSeparator() + "%s", firstQueryString, secondQueryString));
+ String assertionMessage = String.format(
+ "The size of the result sets mismatch (%d and %d)!" + System.lineSeparator()
+ + "First query: \"%s\", whose cardinality is: %d" + System.lineSeparator()
+ + "Second query:\"%s\", whose cardinality is: %d",
+ resultSet.size(), secondResultSet.size(), originalQueryString, resultSet.size(),
+ combinedQueryString, secondResultSet.size());
throw new AssertionError(assertionMessage);
}
Set firstHashSet = new HashSet<>(resultSet);
Set secondHashSet = new HashSet<>(secondResultSet);
- if (!firstHashSet.equals(secondHashSet)) {
+ boolean validateResultSizeOnly = state.getOptions().validateResultSizeOnly();
+ if (!validateResultSizeOnly && !firstHashSet.equals(secondHashSet)) {
Set firstResultSetMisses = new HashSet<>(firstHashSet);
firstResultSetMisses.removeAll(secondHashSet);
Set secondResultSetMisses = new HashSet<>(secondHashSet);
secondResultSetMisses.removeAll(firstHashSet);
- String queryFormatString = "%s; -- misses: %s";
+
+ String queryFormatString = "-- Query: \"%s\"; It misses: \"%s\"";
String firstQueryString = String.format(queryFormatString, originalQueryString, firstResultSetMisses);
- String secondQueryString = String.format(queryFormatString,
- combinedString.stream().collect(Collectors.joining(";")), secondResultSetMisses);
- state.getState().statements.add(new QueryAdapter(firstQueryString));
- state.getState().statements.add(new QueryAdapter(secondQueryString));
- String assertionMessage = String.format("the content of the result sets mismatch!\n%s\n%s",
- firstQueryString, secondQueryString);
+ String secondQueryString = String.format(queryFormatString, String.join(";", combinedString),
+ secondResultSetMisses);
+ // update the SELECT queries to be logged at the bottom of the error log file
+ state.getState().getLocalState()
+ .log(String.format("%s" + System.lineSeparator() + "%s", firstQueryString, secondQueryString));
+ String assertionMessage = String.format("The content of the result sets mismatch!" + System.lineSeparator()
+ + "First query : \"%s\"" + System.lineSeparator() + "Second query: \"%s\"", originalQueryString,
+ secondQueryString);
throw new AssertionError(assertionMessage);
}
}
+ public static void assumeResultSetsAreEqual(List resultSet, List secondResultSet,
+ String originalQueryString, List combinedString, SQLGlobalState, ?> state,
+ UnaryOperator canonicalizationRule) {
+ // Overloaded version of assumeResultSetsAreEqual that takes a canonicalization function which is applied to
+ // both result sets before their comparison.
+ List canonicalizedResultSet = resultSet.stream().map(canonicalizationRule).collect(Collectors.toList());
+ List canonicalizedSecondResultSet = secondResultSet.stream().map(canonicalizationRule)
+ .collect(Collectors.toList());
+ assumeResultSetsAreEqual(canonicalizedResultSet, canonicalizedSecondResultSet, originalQueryString,
+ combinedString, state);
+ }
+
public static List getCombinedResultSet(String firstQueryString, String secondQueryString,
- String thirdQueryString, List combinedString, boolean asUnion, GlobalState> state,
- Set errors) throws SQLException {
+ String thirdQueryString, List combinedString, boolean asUnion, SQLGlobalState, ?> state,
+ ExpectedErrors errors) throws SQLException {
List secondResultSet;
if (asUnion) {
String unionString = firstQueryString + " UNION ALL " + secondQueryString + " UNION ALL "
@@ -138,8 +163,8 @@ public static List getCombinedResultSet(String firstQueryString, String
}
public static List getCombinedResultSetNoDuplicates(String firstQueryString, String secondQueryString,
- String thirdQueryString, List combinedString, boolean asUnion, GlobalState> state,
- Set errors) throws SQLException {
+ String thirdQueryString, List combinedString, boolean asUnion, SQLGlobalState, ?> state,
+ ExpectedErrors errors) throws SQLException {
String unionString;
if (asUnion) {
unionString = firstQueryString + " UNION " + secondQueryString + " UNION " + thirdQueryString;
@@ -153,4 +178,20 @@ public static List getCombinedResultSetNoDuplicates(String firstQueryStr
return secondResultSet;
}
+ public static String canonicalizeResultValue(String value) {
+ if (value == null) {
+ return value;
+ }
+
+ switch (value) {
+ case "-0.0":
+ return "0.0";
+ case "-0":
+ return "0";
+ default:
+ }
+
+ return value;
+ }
+
}
diff --git a/src/sqlancer/CompositeTestOracle.java b/src/sqlancer/CompositeTestOracle.java
deleted file mode 100644
index 9851ee24c..000000000
--- a/src/sqlancer/CompositeTestOracle.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package sqlancer;
-
-import java.sql.SQLException;
-import java.util.List;
-
-public class CompositeTestOracle implements TestOracle {
-
- private final TestOracle[] oracles;
- private int i;
-
- public CompositeTestOracle(List oracles) {
- this.oracles = oracles.toArray(new TestOracle[oracles.size()]);
- }
-
- @Override
- public void check() throws SQLException {
- try {
- oracles[i].check();
- } finally {
- i = (i + 1) % oracles.length;
- }
- }
-}
diff --git a/src/sqlancer/DBMSSpecificOptions.java b/src/sqlancer/DBMSSpecificOptions.java
new file mode 100644
index 000000000..4607557d1
--- /dev/null
+++ b/src/sqlancer/DBMSSpecificOptions.java
@@ -0,0 +1,9 @@
+package sqlancer;
+
+import java.util.List;
+
+public interface DBMSSpecificOptions>> {
+
+ List getTestOracleFactory();
+
+}
diff --git a/src/sqlancer/DatabaseProvider.java b/src/sqlancer/DatabaseProvider.java
index ff323f8bf..d169324fa 100644
--- a/src/sqlancer/DatabaseProvider.java
+++ b/src/sqlancer/DatabaseProvider.java
@@ -1,18 +1,20 @@
package sqlancer;
-import java.io.FileWriter;
-import java.sql.Connection;
-import java.sql.SQLException;
+import sqlancer.common.log.LoggableFactory;
-public interface DatabaseProvider, O> {
+public interface DatabaseProvider, O extends DBMSSpecificOptions>, C extends SQLancerDBConnection> {
/**
* Gets the the {@link GlobalState} class.
+ *
+ * @return the class extending {@link GlobalState}
*/
Class getGlobalStateClass();
/**
* Gets the JCommander option class.
+ *
+ * @return the class representing the DBMS-specific options.
*/
Class getOptionClass();
@@ -22,24 +24,36 @@ public interface DatabaseProvider, O> {
* @param globalState
* the state created and is valid for this method call.
*
+ * @return Reproducer if a bug is found and a reproducer is available.
+ *
+ * @throws Exception
+ * if creating the database fails.
+ *
*/
- void generateAndTestDatabase(G globalState) throws SQLException;
-
- Connection createDatabase(G globalState) throws SQLException;
+ Reproducer generateAndTestDatabase(G globalState) throws Exception;
/**
- * The DBMS name is used to name the log directory and command to test the respective DBMS.
+ * The experimental feature: Query Plan Guidance.
+ *
+ * @param globalState
+ * the state created and is valid for this method call.
+ *
+ * @throws Exception
+ * if testing fails.
+ *
*/
- String getDBMSName();
+ void generateAndTestDatabaseWithQueryPlanGuidance(G globalState) throws Exception;
+
+ C createDatabase(G globalState) throws Exception;
- // TODO: remove this
/**
- * Deprecated method to print the database-specific state, previously used for PQS.
+ * The DBMS name is used to name the log directory and command to test the respective DBMS.
*
- * @param writer
- * @param state
+ * @return the DBMS' name
*/
- void printDatabaseSpecificState(FileWriter writer, StateToReproduce state);
+ String getDBMSName();
+
+ LoggableFactory getLoggableFactory();
StateToReproduce getStateToReproduce(String databaseName);
diff --git a/src/sqlancer/ExecutionTimer.java b/src/sqlancer/ExecutionTimer.java
new file mode 100644
index 000000000..3d88697b3
--- /dev/null
+++ b/src/sqlancer/ExecutionTimer.java
@@ -0,0 +1,23 @@
+package sqlancer;
+
+public final class ExecutionTimer {
+
+ private long startTime;
+ private long endTime;
+
+ public ExecutionTimer start() {
+ startTime = System.currentTimeMillis();
+ return this;
+ }
+
+ public ExecutionTimer end() {
+ endTime = System.currentTimeMillis();
+ return this;
+ }
+
+ public String asString() {
+ long timeMillis = endTime - startTime;
+ return timeMillis + "ms";
+ }
+
+}
diff --git a/src/sqlancer/GlobalState.java b/src/sqlancer/GlobalState.java
index 16c4e6233..2b93012c2 100644
--- a/src/sqlancer/GlobalState.java
+++ b/src/sqlancer/GlobalState.java
@@ -1,42 +1,37 @@
package sqlancer;
-import java.sql.Connection;
+import sqlancer.common.query.Query;
+import sqlancer.common.query.SQLancerResultSet;
+import sqlancer.common.schema.AbstractSchema;
+import sqlancer.common.schema.AbstractTable;
-import sqlancer.Main.QueryManager;
-import sqlancer.Main.StateLogger;
+public abstract class GlobalState, S extends AbstractSchema, ?>, C extends SQLancerDBConnection> {
-/**
- * Represents a global state that is valid for a testing session on a given database.
- *
- * @param
- * the option parameter.
- */
-public class GlobalState {
-
- private Connection con;
+ protected C databaseConnection;
private Randomly r;
private MainOptions options;
- private O dmbsSpecificOptions;
- private StateLogger logger;
+ private O dbmsSpecificOptions;
+ private S schema;
+ private Main.StateLogger logger;
private StateToReproduce state;
- private QueryManager manager;
+ private Main.QueryManager manager;
private String databaseName;
- public void setConnection(Connection con) {
- this.con = con;
+ public void setConnection(C con) {
+ this.databaseConnection = con;
}
- @SuppressWarnings("unchecked")
- public void setDmbsSpecificOptions(Object dmbsSpecificOptions) {
- this.dmbsSpecificOptions = (O) dmbsSpecificOptions;
+ public C getConnection() {
+ return databaseConnection;
}
- public O getDmbsSpecificOptions() {
- return dmbsSpecificOptions;
+ @SuppressWarnings("unchecked")
+ public void setDbmsSpecificOptions(Object dbmsSpecificOptions) {
+ this.dbmsSpecificOptions = (O) dbmsSpecificOptions;
}
- public Connection getConnection() {
- return con;
+ public O getDbmsSpecificOptions() {
+ return dbmsSpecificOptions;
}
public void setRandomly(Randomly r) {
@@ -55,11 +50,11 @@ public void setMainOptions(MainOptions options) {
this.options = options;
}
- public void setStateLogger(StateLogger logger) {
+ public void setStateLogger(Main.StateLogger logger) {
this.logger = logger;
}
- public StateLogger getLogger() {
+ public Main.StateLogger getLogger() {
return logger;
}
@@ -71,11 +66,11 @@ public StateToReproduce getState() {
return state;
}
- public QueryManager getManager() {
+ public Main.QueryManager getManager() {
return manager;
}
- public void setManager(QueryManager manager) {
+ public void setManager(Main.QueryManager manager) {
this.manager = manager;
}
@@ -87,4 +82,72 @@ public void setDatabaseName(String databaseName) {
this.databaseName = databaseName;
}
+ private ExecutionTimer executePrologue(Query> q) throws Exception {
+ boolean logExecutionTime = getOptions().logExecutionTime();
+ ExecutionTimer timer = null;
+ if (logExecutionTime) {
+ timer = new ExecutionTimer().start();
+ }
+ if (getOptions().printAllStatements()) {
+ System.out.println(q.getLogString());
+ }
+ if (getOptions().logEachSelect()) {
+ if (logExecutionTime) {
+ getLogger().writeCurrentNoLineBreak(q.getLogString());
+ } else {
+ getLogger().writeCurrent(q.getLogString());
+ }
+ }
+ return timer;
+ }
+
+ protected abstract void executeEpilogue(Query> q, boolean success, ExecutionTimer timer) throws Exception;
+
+ public boolean executeStatement(Query q, String... fills) throws Exception {
+ ExecutionTimer timer = executePrologue(q);
+ boolean success = manager.execute(q, fills);
+ executeEpilogue(q, success, timer);
+ return success;
+ }
+
+ public SQLancerResultSet executeStatementAndGet(Query q, String... fills) throws Exception {
+ ExecutionTimer timer = executePrologue(q);
+ SQLancerResultSet result = manager.executeAndGet(q, fills);
+ boolean success = result != null;
+ if (success) {
+ result.registerEpilogue(() -> {
+ try {
+ executeEpilogue(q, success, timer);
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ });
+ }
+ return result;
+ }
+
+ public S getSchema() {
+ if (schema == null) {
+ try {
+ updateSchema();
+ } catch (Exception e) {
+ throw new AssertionError(e.getMessage());
+ }
+ }
+ return schema;
+ }
+
+ protected void setSchema(S schema) {
+ this.schema = schema;
+ }
+
+ public void updateSchema() throws Exception {
+ setSchema(readSchema());
+ for (AbstractTable, ?, ?> table : schema.getDatabaseTables()) {
+ table.recomputeCount();
+ }
+ }
+
+ protected abstract S readSchema() throws Exception;
+
}
diff --git a/src/sqlancer/Main.java b/src/sqlancer/Main.java
index ae8f874b7..47ba2aedf 100644
--- a/src/sqlancer/Main.java
+++ b/src/sqlancer/Main.java
@@ -3,13 +3,9 @@
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.StringWriter;
import java.io.Writer;
import java.nio.file.Files;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
+import java.nio.file.Path;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -17,23 +13,41 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.ServiceLoader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.JCommander.Builder;
+import sqlancer.citus.CitusProvider;
import sqlancer.clickhouse.ClickHouseProvider;
import sqlancer.cockroachdb.CockroachDBProvider;
+import sqlancer.common.log.Loggable;
+import sqlancer.common.query.Query;
+import sqlancer.common.query.SQLancerResultSet;
+import sqlancer.databend.DatabendProvider;
+import sqlancer.doris.DorisProvider;
import sqlancer.duckdb.DuckDBProvider;
+import sqlancer.h2.H2Provider;
+import sqlancer.hive.HiveProvider;
+import sqlancer.hsqldb.HSQLDBProvider;
import sqlancer.mariadb.MariaDBProvider;
+import sqlancer.materialize.MaterializeProvider;
import sqlancer.mysql.MySQLProvider;
+import sqlancer.oceanbase.OceanBaseProvider;
import sqlancer.postgres.PostgresProvider;
+import sqlancer.presto.PrestoProvider;
+import sqlancer.questdb.QuestDBProvider;
+import sqlancer.spark.SparkProvider;
import sqlancer.sqlite3.SQLite3Provider;
import sqlancer.tidb.TiDBProvider;
+import sqlancer.yugabyte.ycql.YCQLProvider;
+import sqlancer.yugabyte.ysql.YSQLProvider;
public final class Main {
@@ -42,10 +56,11 @@ public final class Main {
public static volatile AtomicLong nrDatabases = new AtomicLong();
public static volatile AtomicLong nrSuccessfulActions = new AtomicLong();
public static volatile AtomicLong nrUnsuccessfulActions = new AtomicLong();
- static int threadsShutdown;
+ public static volatile AtomicLong threadsShutdown = new AtomicLong();
+ static boolean progressMonitorStarted;
static {
- System.setProperty(org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "ERROR");
+ System.setProperty(org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "ERROR");
if (!LOG_DIRECTORY.exists()) {
LOG_DIRECTORY.mkdir();
}
@@ -58,11 +73,23 @@ public static final class StateLogger {
private final File loggerFile;
private File curFile;
+ private File queryPlanFile;
+ private File reduceFile;
private FileWriter logFileWriter;
public FileWriter currentFileWriter;
+ private FileWriter queryPlanFileWriter;
+ private FileWriter reduceFileWriter;
+ private Path reproduceFilePath;
+ private List> reduceSetupStatements;
+ private String reduceBugInformation;
+ private int nrReductionAttempts;
+
private static final List INITIALIZED_PROVIDER_NAMES = new ArrayList<>();
private final boolean logEachSelect;
- private final DatabaseProvider, ?> provider;
+ private final boolean logQueryPlan;
+
+ private final boolean useReducer;
+ private final DatabaseProvider, ?, ?> databaseProvider;
private static final class AlsoWriteToConsoleFileWriter extends FileWriter {
@@ -83,8 +110,7 @@ public void write(String str) throws IOException {
}
}
- public StateLogger(String databaseName, DatabaseProvider, ?> provider, MainOptions options) {
- this.provider = provider;
+ public StateLogger(String databaseName, DatabaseProvider, ?, ?> provider, MainOptions options) {
File dir = new File(LOG_DIRECTORY, provider.getDBMSName());
if (dir.exists() && !dir.isDirectory()) {
throw new AssertionError(dir);
@@ -95,27 +121,49 @@ public StateLogger(String databaseName, DatabaseProvider, ?> provider, MainOpt
if (logEachSelect) {
curFile = new File(dir, databaseName + "-cur.log");
}
+ logQueryPlan = options.logQueryPlan();
+ if (logQueryPlan) {
+ queryPlanFile = new File(dir, databaseName + "-plan.log");
+ }
+ this.useReducer = options.useReducer();
+ if (useReducer) {
+ File reduceFileDir = new File(dir, "reduce");
+ if (!reduceFileDir.exists()) {
+ reduceFileDir.mkdir();
+ }
+ this.reduceFile = new File(reduceFileDir, databaseName + "-reduce.log");
+ }
+ if (options.serializeReproduceState()) {
+ File reproduceFileDir = new File(dir, "reproduce");
+ if (!reproduceFileDir.exists()) {
+ reproduceFileDir.mkdir();
+ }
+ reproduceFilePath = new File(reproduceFileDir, databaseName + ".ser").toPath();
+ }
+ this.databaseProvider = provider;
}
- private synchronized void ensureExistsAndIsEmpty(File dir, DatabaseProvider, ?> provider) {
+ private void ensureExistsAndIsEmpty(File dir, DatabaseProvider, ?, ?> provider) {
if (INITIALIZED_PROVIDER_NAMES.contains(provider.getDBMSName())) {
return;
}
- if (!dir.exists()) {
- try {
- Files.createDirectories(dir.toPath());
- } catch (IOException e) {
- throw new AssertionError(e);
+ synchronized (INITIALIZED_PROVIDER_NAMES) {
+ if (!dir.exists()) {
+ try {
+ Files.createDirectories(dir.toPath());
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
}
- }
- File[] listFiles = dir.listFiles();
- assert listFiles != null : "directory was just created, so it should exist";
- for (File file : listFiles) {
- if (!file.isDirectory()) {
- file.delete();
+ File[] listFiles = dir.listFiles();
+ assert listFiles != null : "directory was just created, so it should exist";
+ for (File file : listFiles) {
+ if (!file.isDirectory()) {
+ file.delete();
+ }
}
+ INITIALIZED_PROVIDER_NAMES.add(provider.getDBMSName());
}
- INITIALIZED_PROVIDER_NAMES.add(provider.getDBMSName());
}
private FileWriter getLogFileWriter() {
@@ -143,6 +191,34 @@ public FileWriter getCurrentFileWriter() {
return currentFileWriter;
}
+ public FileWriter getQueryPlanFileWriter() {
+ if (!logQueryPlan) {
+ throw new UnsupportedOperationException();
+ }
+ if (queryPlanFileWriter == null) {
+ try {
+ queryPlanFileWriter = new FileWriter(queryPlanFile, true);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+ return queryPlanFileWriter;
+ }
+
+ public FileWriter getReduceFileWriter() {
+ if (!useReducer) {
+ throw new UnsupportedOperationException();
+ }
+ if (reduceFileWriter == null) {
+ try {
+ reduceFileWriter = new FileWriter(reduceFile, false);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+ return reduceFileWriter;
+ }
+
public void writeCurrent(StateToReproduce state) {
if (!logEachSelect) {
throw new UnsupportedOperationException();
@@ -156,33 +232,92 @@ public void writeCurrent(StateToReproduce state) {
}
}
- public void writeCurrent(String queryString) {
+ public void writeCurrent(String input) {
+ write(databaseProvider.getLoggableFactory().createLoggable(input));
+ }
+
+ public void writeCurrentNoLineBreak(String input) {
+ write(databaseProvider.getLoggableFactory().createLoggableWithNoLinebreak(input));
+ }
+
+ private void write(Loggable loggable) {
if (!logEachSelect) {
throw new UnsupportedOperationException();
}
try {
- getCurrentFileWriter().write(queryString + ";\n");
+ getCurrentFileWriter().write(loggable.getLogString());
+
currentFileWriter.flush();
} catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ throw new AssertionError();
}
}
- public void logRowNotFound(StateToReproduce state) {
- printState(getLogFileWriter(), state);
+ public void writeQueryPlan(String queryPlan) {
+ if (!logQueryPlan) {
+ throw new UnsupportedOperationException();
+ }
try {
- getLogFileWriter().flush();
+ getQueryPlanFileWriter().append(removeNamesFromQueryPlans(queryPlan));
+ queryPlanFileWriter.flush();
+ } catch (IOException e) {
+ throw new AssertionError();
+ }
+ }
+
+ public void setReductionContext(List> setupStatements, String bugInformation) {
+ this.reduceSetupStatements = setupStatements;
+ this.reduceBugInformation = bugInformation;
+ }
+
+ public void logReduced(StateToReproduce state) {
+ nrReductionAttempts++;
+ logReduced(state, "Reduction attempt " + nrReductionAttempts
+ + ": the bug was still triggered with the following statements");
+ }
+
+ public void logReduced(StateToReproduce state, String description) {
+ FileWriter reduceFileWriter = getReduceFileWriter();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("-- ").append(description).append(System.lineSeparator());
+ if (reduceSetupStatements != null && !reduceSetupStatements.isEmpty()) {
+ appendStatements(sb, reduceSetupStatements);
+ // e.g. DROP DATABASE IF EXISTS db; CREATE DATABASE db; USE db;
+ // these statements are executed at the start of every test case and are never reduced
+ }
+ appendStatements(sb, state.getStatements());
+ if (reduceBugInformation != null) {
+ sb.append(reduceBugInformation);
+ }
+ sb.append(System.lineSeparator());
+ try {
+ reduceFileWriter.write(sb.toString());
+
} catch (IOException e) {
throw new AssertionError(e);
+ } finally {
+ try {
+ reduceFileWriter.flush();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ private void appendStatements(StringBuilder sb, List> statements) {
+ for (Query> s : statements) {
+ sb.append(databaseProvider.getLoggableFactory().createLoggable(s.getLogString()).getLogString());
}
}
public void logException(Throwable reduce, StateToReproduce state) {
- String stackTrace = getStackTrace(reduce);
+ Loggable stackTrace = getStackTrace(reduce);
FileWriter logFileWriter2 = getLogFileWriter();
try {
- logFileWriter2.write(stackTrace);
+ logFileWriter2.write(stackTrace.getLogString());
printState(logFileWriter2, state);
} catch (IOException e) {
throw new AssertionError(e);
@@ -190,100 +325,105 @@ public void logException(Throwable reduce, StateToReproduce state) {
try {
logFileWriter2.flush();
} catch (IOException e) {
- // TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- private String getStackTrace(Throwable e1) {
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- e1.printStackTrace(pw);
- return "--" + sw.toString().replace("\n", "\n--");
+ private Loggable getStackTrace(Throwable e1) {
+ return databaseProvider.getLoggableFactory().convertStacktraceToLoggable(e1);
}
private void printState(FileWriter writer, StateToReproduce state) {
StringBuilder sb = new StringBuilder();
- DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
- Date date = new Date();
- sb.append("-- Time: " + dateFormat.format(date) + "\n");
- sb.append("-- Database: " + state.getDatabaseName() + "\n");
- sb.append("-- Database version: " + state.getDatabaseVersion() + "\n");
- sb.append("-- seed value: " + state.getSeedValue() + "\n");
- for (Query s : state.getStatements()) {
- if (s.getQueryString().endsWith(";")) {
- sb.append(s.getQueryString());
- } else {
- sb.append(s.getQueryString() + ";");
- }
- sb.append('\n');
- }
- if (state.getQueryString() != null) {
- sb.append(state.getQueryString() + ";\n");
+
+ sb.append(databaseProvider.getLoggableFactory()
+ .getInfo(state.getDatabaseName(), state.getDatabaseVersion(), state.getSeedValue()).getLogString());
+
+ for (Query> s : state.getStatements()) {
+ sb.append(databaseProvider.getLoggableFactory().createLoggable(s.getLogString()).getLogString());
}
try {
writer.write(sb.toString());
} catch (IOException e) {
throw new AssertionError(e);
}
- provider.printDatabaseSpecificState(writer, state);
}
+ private String removeNamesFromQueryPlans(String queryPlan) {
+ String result = queryPlan;
+ result = result.replaceAll("t[0-9]+", "t0"); // Avoid duplicate tables
+ result = result.replaceAll("v[0-9]+", "v0"); // Avoid duplicate views
+ result = result.replaceAll("i[0-9]+", "i0"); // Avoid duplicate indexes
+ return result + "\n";
+ }
+
+ public Path getReproduceFilePath() {
+ return reproduceFilePath;
+ }
}
- public static class QueryManager {
+ public static class QueryManager {
- private final GlobalState> globalState;
+ private final GlobalState, ?, C> globalState;
- QueryManager(GlobalState> globalState) {
+ QueryManager(GlobalState, ?, C> globalState) {
this.globalState = globalState;
}
- public boolean execute(Query q) throws SQLException {
- globalState.getState().statements.add(q);
- boolean success = q.execute(globalState);
+ public boolean execute(Query q, String... fills) throws Exception {
+ boolean success;
+ success = q.execute(globalState, fills);
Main.nrSuccessfulActions.addAndGet(1);
+ if (globalState.getOptions().loggerPrintFailed() || success) {
+ globalState.getState().logStatement(q);
+ }
return success;
}
+ public SQLancerResultSet executeAndGet(Query q, String... fills) throws Exception {
+ globalState.getState().logStatement(q);
+ SQLancerResultSet result;
+ result = q.executeAndGet(globalState, fills);
+ Main.nrSuccessfulActions.addAndGet(1);
+ return result;
+ }
+
public void incrementSelectQueryCount() {
Main.nrQueries.addAndGet(1);
}
+ public Long getSelectQueryCount() {
+ return Main.nrQueries.get();
+ }
+
public void incrementCreateDatabase() {
Main.nrDatabases.addAndGet(1);
}
}
- public static void printArray(Object... arr) {
- for (Object o : arr) {
- System.out.println(o);
- }
- }
-
public static void main(String[] args) {
System.exit(executeMain(args));
}
- public static class DBMSExecutor, O> {
+ public static class DBMSExecutor, O extends DBMSSpecificOptions>, C extends SQLancerDBConnection> {
- private final DatabaseProvider provider;
+ private final DatabaseProvider provider;
private final MainOptions options;
private final O command;
private final String databaseName;
- private final long seed;
private StateLogger logger;
private StateToReproduce stateToRepro;
+ private final Randomly r;
- public DBMSExecutor(DatabaseProvider provider, MainOptions options, O dbmsSpecificOptions,
- String databaseName, long seed) {
+ public DBMSExecutor(DatabaseProvider provider, MainOptions options, O dbmsSpecificOptions,
+ String databaseName, Randomly r) {
this.provider = provider;
this.options = options;
this.databaseName = databaseName;
- this.seed = seed;
this.command = dbmsSpecificOptions;
+ this.r = r;
}
private G createGlobalState() {
@@ -298,32 +438,136 @@ public O getCommand() {
return command;
}
- public void run() throws SQLException {
+ public void testConnection() throws Exception {
+ G state = getInitializedGlobalState(options.getRandomSeed());
+ try (SQLancerDBConnection con = provider.createDatabase(state)) {
+ return;
+ }
+ }
+
+ public void run() throws Exception {
G state = createGlobalState();
stateToRepro = provider.getStateToReproduce(databaseName);
- stateToRepro.seedValue = seed;
+ stateToRepro.seedValue = r.getSeed();
state.setState(stateToRepro);
logger = new StateLogger(databaseName, provider, options);
- Randomly r = new Randomly(seed);
state.setRandomly(r);
state.setDatabaseName(databaseName);
state.setMainOptions(options);
- state.setDmbsSpecificOptions(command);
- try (Connection con = provider.createDatabase(state)) {
- QueryManager manager = new QueryManager(state);
+ state.setDbmsSpecificOptions(command);
+ try (C con = provider.createDatabase(state)) {
+ QueryManager manager = new QueryManager<>(state);
try {
- java.sql.DatabaseMetaData meta = con.getMetaData();
- stateToRepro.databaseVersion = meta.getDatabaseProductVersion();
- } catch (SQLFeatureNotSupportedException e) {
+ stateToRepro.databaseVersion = con.getDatabaseVersion();
+ } catch (Exception e) {
// ignore
}
state.setConnection(con);
state.setStateLogger(logger);
state.setManager(manager);
- provider.generateAndTestDatabase(state);
+ if (options.logEachSelect()) {
+ logger.writeCurrent(state.getState());
+ }
+ // statements logged so far stem from the database setup (e.g., DROP DATABASE IF
+ // EXISTS, CREATE DATABASE, USE), performed by createDatabase
+ int nrSetupStatements = stateToRepro.getStatements().size();
+ Reproducer reproducer = null;
+ if (options.enableQPG()) {
+ provider.generateAndTestDatabaseWithQueryPlanGuidance(state);
+ } else {
+ reproducer = provider.generateAndTestDatabase(state);
+ }
+ try {
+ logger.getCurrentFileWriter().close();
+ logger.currentFileWriter = null;
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+
+ if (options.serializeReproduceState() && reproducer != null) {
+ stateToRepro.serialize(logger.getReproduceFilePath());
+ }
+ if (options.reduceAST() && !options.useReducer()) {
+ throw new AssertionError("To reduce AST, use-reducer option must be enabled first");
+ }
+ if (options.useReducer()) {
+ if (reproducer == null) {
+ logger.getReduceFileWriter().write("current oracle does not support experimental reducer.");
+ throw new IgnoreMeException();
+ }
+
+ // reduce only the generation statements: the database setup (logged by
+ // createDatabase) is re-executed by the reducers for every candidate, and the
+ // oracle queries (logged by the oracle's local state) by the reproducer
+ List> allStatements = new ArrayList<>(stateToRepro.getStatements());
+ List> setupStatements = new ArrayList<>(allStatements.subList(0, nrSetupStatements));
+ List> oracleQueryStatements = stateToRepro.getLocalState() == null ? new ArrayList<>()
+ : new ArrayList<>(stateToRepro.getLocalState().getStatements());
+ stateToRepro.setStatements(new ArrayList<>(allStatements.subList(nrSetupStatements,
+ allStatements.size() - oracleQueryStatements.size())));
+
+ G newGlobalState = createGlobalState();
+ newGlobalState.setState(stateToRepro);
+ newGlobalState.setRandomly(r);
+ newGlobalState.setDatabaseName(databaseName);
+ newGlobalState.setMainOptions(options);
+ newGlobalState.setDbmsSpecificOptions(command);
+ QueryManager newManager = new QueryManager<>(newGlobalState);
+ newGlobalState.setStateLogger(new StateLogger(databaseName, provider, options));
+ newGlobalState.setManager(newManager);
+ newGlobalState.getLogger().setReductionContext(setupStatements, reproducer.getBugInformation());
+
+ Reducer reducer = new StatementReducer<>(provider);
+ reducer.reduce(state, reproducer, newGlobalState);
+
+ if (options.reduceAST()) {
+ Reducer astBasedReducer = new ASTBasedReducer<>(provider);
+ astBasedReducer.reduce(state, reproducer, newGlobalState);
+ }
+
+ // reassemble the statements so that the main log looks like one produced
+ // without the reducer, with the generation statements replaced by the reduced
+ // ones and the oracle queries at the end
+ List> finalStatements = new ArrayList<>(setupStatements);
+ finalStatements.addAll(stateToRepro.getStatements());
+ finalStatements.addAll(oracleQueryStatements);
+ stateToRepro.setStatements(finalStatements);
+ String bugInformation = reproducer.getBugInformation();
+ if (bugInformation != null) {
+ for (String line : bugInformation.split(System.lineSeparator())) {
+ stateToRepro.logStatement(line);
+ }
+ }
+
+ StateLogger reduceLogger = newGlobalState.getLogger();
+ if (reduceLogger.reduceFileWriter != null) {
+ try {
+ reduceLogger.reduceFileWriter.close();
+ reduceLogger.reduceFileWriter = null;
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ throw new AssertionError("Found a potential bug, please check reducer log for detail.");
+ }
}
}
+ private G getInitializedGlobalState(long seed) {
+ G state = createGlobalState();
+ stateToRepro = provider.getStateToReproduce(databaseName);
+ stateToRepro.seedValue = seed;
+ state.setState(stateToRepro);
+ logger = new StateLogger(databaseName, provider, options);
+ Randomly r = new Randomly(seed);
+ state.setRandomly(r);
+ state.setDatabaseName(databaseName);
+ state.setMainOptions(options);
+ state.setDbmsSpecificOptions(command);
+ return state;
+ }
+
public StateLogger getLogger() {
return logger;
}
@@ -333,13 +577,13 @@ public StateToReproduce getStateToReproduce() {
}
}
- public static class DBMSExecutorFactory, O> {
+ public static class DBMSExecutorFactory, O extends DBMSSpecificOptions>, C extends SQLancerDBConnection> {
- private final DatabaseProvider provider;
+ private final DatabaseProvider provider;
private final MainOptions options;
private final O command;
- public DBMSExecutorFactory(DatabaseProvider provider, MainOptions options) {
+ public DBMSExecutorFactory(DatabaseProvider provider, MainOptions options) {
this.provider = provider;
this.options = options;
this.command = createCommand();
@@ -358,54 +602,93 @@ public O getCommand() {
}
@SuppressWarnings("unchecked")
- public DBMSExecutor getDBMSExecutor(String databaseName, long seed) {
+ public DBMSExecutor getDBMSExecutor(String databaseName, Randomly r) {
try {
- return new DBMSExecutor(provider.getClass().getDeclaredConstructor().newInstance(), options,
- command, databaseName, seed);
+ return new DBMSExecutor(provider.getClass().getDeclaredConstructor().newInstance(), options,
+ command, databaseName, r);
} catch (Exception e) {
throw new AssertionError(e);
}
}
+ public DatabaseProvider getProvider() {
+ return provider;
+ }
+
}
public static int executeMain(String... args) throws AssertionError {
- List> providers = getDBMSProviders();
- Map> nameToProvider = new HashMap<>();
+ List> providers = getDBMSProviders();
+ Map> nameToProvider = new HashMap<>();
MainOptions options = new MainOptions();
Builder commandBuilder = JCommander.newBuilder().addObject(options);
- for (DatabaseProvider, ?> provider : providers) {
+ for (DatabaseProvider, ?, ?> provider : providers) {
String name = provider.getDBMSName();
- if (!name.toLowerCase().equals(name)) {
- throw new AssertionError(name + " should be in lowercase!");
- }
- DBMSExecutorFactory, ?> executorFactory = new DBMSExecutorFactory<>(provider, options);
+ DBMSExecutorFactory, ?, ?> executorFactory = new DBMSExecutorFactory<>(provider, options);
commandBuilder = commandBuilder.addCommand(name, executorFactory.getCommand());
nameToProvider.put(name, executorFactory);
}
JCommander jc = commandBuilder.programName("SQLancer").build();
jc.parse(args);
- if (jc.getParsedCommand() == null) {
+ if (jc.getParsedCommand() == null || options.isHelp()) {
jc.usage();
return options.getErrorExitCode();
}
+ Randomly.initialize(options);
if (options.printProgressInformation()) {
startProgressMonitor();
+ if (options.printProgressSummary()) {
+ Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ System.out.println("Overall execution statistics");
+ System.out.println("============================");
+ System.out.println(formatInteger(nrQueries.get()) + " queries");
+ System.out.println(formatInteger(nrDatabases.get()) + " databases");
+ System.out.println(
+ formatInteger(nrSuccessfulActions.get()) + " successfully-executed statements");
+ System.out.println(
+ formatInteger(nrUnsuccessfulActions.get()) + " unsuccessfully-executed statements");
+ }
+
+ private String formatInteger(long intValue) {
+ if (intValue > 1000) {
+ return String.format("%,9dk", intValue / 1000);
+ } else {
+ return String.format("%,10d", intValue);
+ }
+ }
+ }));
+ }
}
ExecutorService execService = Executors.newFixedThreadPool(options.getNumberConcurrentThreads());
- DBMSExecutorFactory, ?> executorFactory = nameToProvider.get(jc.getParsedCommand());
+ DBMSExecutorFactory, ?, ?> executorFactory = nameToProvider.get(jc.getParsedCommand());
+
+ if (options.performConnectionTest()) {
+ try {
+ executorFactory.getDBMSExecutor(options.getDatabasePrefix() + "connectiontest", new Randomly())
+ .testConnection();
+ } catch (Exception e) {
+ System.err.println(
+ "SQLancer failed creating a test database, indicating that SQLancer might have failed connecting to the DBMS. In order to change the username, password, host and port, you can use the --username, --password, --host and --port options.\n\n");
+ e.printStackTrace();
+ return options.getErrorExitCode();
+ }
+ }
+ final AtomicBoolean someOneFails = new AtomicBoolean(false);
+
for (int i = 0; i < options.getTotalNumberTries(); i++) {
- final String databaseName = "database" + i;
+ final String databaseName = options.getDatabasePrefix() + i;
final long seed;
if (options.getRandomSeed() == -1) {
seed = System.currentTimeMillis() + i;
} else {
seed = options.getRandomSeed() + i;
}
-
execService.execute(new Runnable() {
@Override
@@ -415,33 +698,53 @@ public void run() {
}
private void runThread(final String databaseName) {
- while (true) {
- DBMSExecutor, ?> executor = executorFactory.getDBMSExecutor(databaseName, seed);
+ Randomly r = new Randomly(seed);
+ try {
+ int maxNrDbs = options.getMaxGeneratedDatabases();
+ // run without a limit if maxNrDbs == -1
+ for (int i = 0; i < maxNrDbs || maxNrDbs == -1; i++) {
+ Boolean continueRunning = run(options, execService, executorFactory, r, databaseName);
+ if (!continueRunning) {
+ someOneFails.set(true);
+ break;
+ }
+ }
+ } finally {
+ threadsShutdown.addAndGet(1);
+ if (threadsShutdown.get() == options.getTotalNumberTries()) {
+ execService.shutdown();
+ }
+ }
+ }
+
+ private boolean run(MainOptions options, ExecutorService execService,
+ DBMSExecutorFactory, ?, ?> executorFactory, Randomly r, final String databaseName) {
+ DBMSExecutor, ?, ?> executor = executorFactory.getDBMSExecutor(databaseName, r);
+ try {
+ executor.run();
+ return true;
+ } catch (IgnoreMeException e) {
+ return true;
+ } catch (Throwable reduce) {
+ reduce.printStackTrace();
+ executor.getStateToReproduce().exception = reduce.getMessage();
+ executor.getLogger().logFileWriter = null;
+ executor.getLogger().logException(reduce, executor.getStateToReproduce());
+ if (options.serializeReproduceState()) {
+ executor.getStateToReproduce().logStatement(reduce.getMessage()); // add the error statement
+ executor.getStateToReproduce().serialize(executor.getLogger().getReproduceFilePath());
+ }
+ return false;
+ } finally {
try {
- executor.run();
- } catch (IgnoreMeException e) {
- continue;
- } catch (Throwable reduce) {
- reduce.printStackTrace();
- executor.getStateToReproduce().exception = reduce.getMessage();
- executor.getLogger().logFileWriter = null;
- executor.getLogger().logException(reduce, executor.getStateToReproduce());
- threadsShutdown++;
- break;
- } finally {
- try {
- if (options.logEachSelect()) {
- if (executor.getLogger().currentFileWriter != null) {
- executor.getLogger().currentFileWriter.close();
- }
- executor.getLogger().currentFileWriter = null;
+ if (options.logEachSelect()) {
+ if (executor.getLogger().currentFileWriter != null) {
+ executor.getLogger().currentFileWriter.close();
}
- } catch (IOException e) {
- e.printStackTrace();
- }
- if (threadsShutdown == options.getTotalNumberTries()) {
- execService.shutdown();
+ executor.getLogger().currentFileWriter = null;
}
+ } catch (IOException e) {
+ e.printStackTrace();
}
}
}
@@ -456,23 +759,68 @@ private void runThread(final String databaseName) {
} catch (InterruptedException e) {
e.printStackTrace();
}
- return threadsShutdown == 0 ? 0 : options.getErrorExitCode();
+
+ return someOneFails.get() ? options.getErrorExitCode() : 0;
}
- static List> getDBMSProviders() {
- List> providers = new ArrayList<>();
- providers.add(new SQLite3Provider());
- providers.add(new CockroachDBProvider());
- providers.add(new MySQLProvider());
- providers.add(new MariaDBProvider());
- providers.add(new TiDBProvider());
- providers.add(new PostgresProvider());
- providers.add(new ClickHouseProvider());
- providers.add(new DuckDBProvider());
+ /**
+ * To register a new provider, it is necessary to implement the DatabaseProvider interface and add an additional
+ * configuration file, see https://docs.oracle.com/javase/9/docs/api/java/util/ServiceLoader.html. Currently, we use
+ * an @AutoService annotation to create the configuration file automatically. This allows SQLancer to pick up
+ * providers in other JARs on the classpath.
+ *
+ * @return The list of service providers on the classpath
+ */
+ static List> getDBMSProviders() {
+ List> providers = new ArrayList<>();
+ @SuppressWarnings("rawtypes")
+ ServiceLoader loader = ServiceLoader.load(DatabaseProvider.class);
+ for (DatabaseProvider, ?, ?> provider : loader) {
+ providers.add(provider);
+ }
+ checkForIssue799(providers);
return providers;
}
- private static void startProgressMonitor() {
+ // see https://github.com/sqlancer/sqlancer/issues/799
+ private static void checkForIssue799(List> providers) {
+ if (providers.isEmpty()) {
+ System.err.println(
+ "No DBMS implementations (i.e., instantiations of the DatabaseProvider class) were found. You likely ran into an issue described in https://github.com/sqlancer/sqlancer/issues/799. As a workaround, I now statically load all supported providers as of June 7, 2023.");
+ providers.add(new CitusProvider());
+ providers.add(new ClickHouseProvider());
+ providers.add(new CockroachDBProvider());
+ providers.add(new DatabendProvider());
+ providers.add(new DorisProvider());
+ providers.add(new DuckDBProvider());
+ providers.add(new H2Provider());
+ providers.add(new HiveProvider());
+ providers.add(new SparkProvider());
+ providers.add(new HSQLDBProvider());
+ providers.add(new MariaDBProvider());
+ providers.add(new MaterializeProvider());
+ providers.add(new MySQLProvider());
+ providers.add(new OceanBaseProvider());
+ providers.add(new PrestoProvider());
+ providers.add(new PostgresProvider());
+ providers.add(new QuestDBProvider());
+ providers.add(new SQLite3Provider());
+ providers.add(new TiDBProvider());
+ providers.add(new YCQLProvider());
+ providers.add(new YSQLProvider());
+ }
+ }
+
+ private static synchronized void startProgressMonitor() {
+ if (progressMonitorStarted) {
+ /*
+ * it might be already started if, for example, the main method is called multiple times in a test (see
+ * https://github.com/sqlancer/sqlancer/issues/90).
+ */
+ return;
+ } else {
+ progressMonitorStarted = true;
+ }
final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(new Runnable() {
@@ -500,7 +848,7 @@ public void run() {
System.out.println(String.format(
"[%s] Executed %d queries (%d queries/s; %.2f/s dbs, successful statements: %2d%%). Threads shut down: %d.",
dateFormat.format(date), currentNrQueries, (int) throughput, throughputDbs,
- successfulStatementsRatio, threadsShutdown));
+ successfulStatementsRatio, threadsShutdown.get()));
timeMillis = System.currentTimeMillis();
lastNrQueries = currentNrQueries;
lastNrDbs = currentNrDbs;
diff --git a/src/sqlancer/MainOptions.java b/src/sqlancer/MainOptions.java
index 6eeac6440..25b769312 100644
--- a/src/sqlancer/MainOptions.java
+++ b/src/sqlancer/MainOptions.java
@@ -1,11 +1,20 @@
package sqlancer;
-import com.beust.jcommander.IStringConverter;
+import java.util.Objects;
+
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
+import sqlancer.Randomly.StringGenerationStrategy;
+
@Parameters(separators = "=", commandDescription = "Options applicable to all DBMS")
public class MainOptions {
+ public static final int NO_SET_PORT = -1;
+ public static final int NO_REDUCE_LIMIT = -1;
+ public static final MainOptions DEFAULT_OPTIONS = new MainOptions();
+
+ @Parameter(names = { "--help", "-h" }, description = "Lists all supported options and commands", help = true)
+ private boolean help; // NOPMD
@Parameter(names = {
"--num-threads" }, description = "How many threads should run concurrently to test separate databases")
@@ -36,21 +45,111 @@ public class MainOptions {
@Parameter(names = "--log-each-select", description = "Logs every statement issued", arity = 1)
private boolean logEachSelect = true; // NOPMD
+ @Parameter(names = "--log-execution-time", description = "Logs the execution time of each statement (requires --log-each-select to be enabled)", arity = 1)
+ private boolean logExecutionTime = true; // NOPMD
+
+ @Parameter(names = "--print-failed", description = "Logs failed insert, create and other statements without results", arity = 1)
+ private boolean loggerPrintFailed = true; // NOPMD
+
+ @Parameter(names = "--qpg-enable", description = "Enable the experimental feature Query Plan Guidance (QPG)", arity = 1)
+ private boolean enableQPG;
+
+ @Parameter(names = "--qpg-log-query-plan", description = "Logs the query plans of each query (requires --qpg-enable)", arity = 1)
+ private boolean logQueryPlan;
+
+ @Parameter(names = "--qpg-max-interval", description = "The maximum number of iterations to mutate tables if no new query plans (requires --qpg-enable)")
+ private static int qpgMaxInterval = 1000;
+
+ @Parameter(names = "--qpg-reward-weight", description = "The weight (0-1) of last reward when updating weighted average reward. A higher value denotes average reward is more affected by the last reward (requires --qpg-enable)")
+ private static double qpgk = 0.25;
+
+ @Parameter(names = "--qpg-selection-probability", description = "The probability (0-1) of the random selection of mutators. A higher value (>0.5) favors exploration over exploitation. (requires --qpg-enable)")
+ private static double qpgProbability = 0.7;
+
@Parameter(names = "--username", description = "The user name used to log into the DBMS")
private String userName = "sqlancer"; // NOPMD
@Parameter(names = "--password", description = "The password used to log into the DBMS")
private String password = "sqlancer"; // NOPMD
+ @Parameter(names = "--host", description = "The host used to log into the DBMS")
+ private String host = null; // NOPMD
+
+ @Parameter(names = "--port", description = "The port used to log into the DBMS")
+ private int port = MainOptions.NO_SET_PORT; // NOPMD
+
@Parameter(names = "--print-progress-information", description = "Whether to print progress information such as the number of databases generated or queries issued", arity = 1)
private boolean printProgressInformation = true; // NOPMD
+ @Parameter(names = "--print-progress-summary", description = "Whether to print an execution summary when exiting SQLancer", arity = 1)
+ private boolean printProgressSummary; // NOPMD
+
@Parameter(names = "--timeout-seconds", description = "The timeout in seconds")
private int timeoutSeconds = -1; // NOPMD
+ @Parameter(names = "--max-generated-databases", description = "The maximum number of databases that are generated by each thread")
+ private int maxGeneratedDatabases = -1; // NOPMD
+
@Parameter(names = "--exit-code-error", description = "The exit code that should be returned when an error is encountered (or a bug is found)")
private int errorExitCode = -1; // NOPMD
+ @Parameter(names = "--print-statements", description = "Print all statements to stdout, before they are sent to the DBMS (not yet implemented for all oracles)", arity = 1)
+ private boolean printStatements; // NOPMD
+
+ @Parameter(names = "--print-succeeding-statements", description = "Print statements that are successfully processed by the DBMS to stdout (not yet implemented for all oracles)", arity = 1)
+ private boolean printSucceedingStatements; // NOPMD
+
+ @Parameter(names = "--test-only-nonempty-tables", description = "Test only databases each of whose tables contain at least a single row", arity = 1)
+ private boolean testOnlyWithMoreThanZeroRows; // NOPMD
+
+ @Parameter(names = "--pqs-test-aggregates", description = "Partially test aggregate functions when all tables contain only a single row.", arity = 1)
+ private boolean testAggregateFunctions; // NOPMD
+
+ @Parameter(names = "--random-string-generation", description = "Select the random-string eneration approach")
+ private StringGenerationStrategy randomStringGenerationStrategy = StringGenerationStrategy.SOPHISTICATED; // NOPMD
+
+ @Parameter(names = "--string-constant-max-length", description = "Specify the maximum-length of generated string constants")
+ private int maxStringConstantLength = 10; // NOPMD
+
+ @Parameter(names = "--use-constant-caching", description = "Specifies whether constants should be cached and re-used with a certain probability", arity = 1)
+ private boolean useConstantCaching = true; // NOPMD
+
+ @Parameter(names = "--use-connection-test", description = "Test whether the DBMS is accessible before trying to connect using multiple threads", arity = 1)
+ private boolean useConnectionTest = true; // NOPMD
+
+ @Parameter(names = "--constant-cache-size", description = "Specifies the size of the constant cache. This option only takes effect when constant caching is enabled")
+ private int constantCacheSize = 100; // NOPMD
+
+ @Parameter(names = "--database-prefix", description = "The prefix used for each database created")
+ private String databasePrefix = "database"; // NOPMD
+
+ @Parameter(names = "--serialize-reproduce-state", description = "Serialize the state to reproduce")
+ private boolean serializeReproduceState = false; // NOPMD
+
+ @Parameter(names = "--use-reducer", description = "EXPERIMENTAL Attempt to reduce queries using a simple reducer. Implemented for TLP WHERE and NoREC only")
+ private boolean useReducer = false; // NOPMD
+
+ @Parameter(names = "--reduce-ast", description = "EXPERIMENTAL Perform AST reduction after statement reduction")
+ private boolean reduceAST = false; // NOPMD
+
+ @Parameter(names = "--statement-reducer-max-steps", description = "EXPERIMENTAL Maximum steps the statement reducer will do")
+ private long maxStatementReduceSteps = NO_REDUCE_LIMIT; // NOPMD
+
+ @Parameter(names = "--statement-reducer-max-time", description = "EXPERIMENTAL Maximum time duration (secs) the AST-based reducer will do")
+ private long maxASTReduceTime = NO_REDUCE_LIMIT; // NOPMD
+
+ @Parameter(names = "--ast-reducer-max-steps", description = "EXPERIMENTAL Maximum steps the AST-based reducer will do")
+ private long maxASTReduceSteps = NO_REDUCE_LIMIT; // NOPMD
+
+ @Parameter(names = "--ast-reducer-max-time", description = "EXPERIMENTAL Maximum time duration (secs) the statement reducer will do")
+ private long maxStatementReduceTime = NO_REDUCE_LIMIT; // NOPMD
+
+ @Parameter(names = "--validate-result-size-only", description = "Should validate result size only and skip comparing content of the result set ", arity = 1)
+ private boolean validateResultSizeOnly = false; // NOPMD
+
+ @Parameter(names = "--canonicalize-sql-strings", description = "Should canonicalize query string (add ';' at the end", arity = 1)
+ private boolean canonicalizeSqlString = true; // NOPMD
+
public int getMaxExpressionDepth() {
return maxExpressionDepth;
}
@@ -67,6 +166,51 @@ public boolean logEachSelect() {
return logEachSelect;
}
+ public boolean printAllStatements() {
+ if (printSucceedingStatements && printStatements) {
+ throw new AssertionError();
+ }
+ return printStatements;
+ }
+
+ public boolean printSucceedingStatements() {
+ if (printStatements && printSucceedingStatements) {
+ throw new AssertionError();
+ }
+ return printSucceedingStatements;
+ }
+
+ public boolean logExecutionTime() {
+ if (!logEachSelect) {
+ throw new AssertionError();
+ }
+ return logExecutionTime;
+ }
+
+ public boolean loggerPrintFailed() {
+ return loggerPrintFailed;
+ }
+
+ public boolean logQueryPlan() {
+ return logQueryPlan;
+ }
+
+ public boolean enableQPG() {
+ return enableQPG;
+ }
+
+ public int getQPGMaxMutationInterval() {
+ return qpgMaxInterval;
+ }
+
+ public double getQPGk() {
+ return qpgk;
+ }
+
+ public double getQPGProbability() {
+ return qpgProbability;
+ }
+
public int getNrQueries() {
return nrQueries;
}
@@ -79,10 +223,6 @@ public int getNrStatementRetryCount() {
return nrStatementRetryCount;
}
- public enum DBMS {
- MariaDB, SQLite3, MySQL, PostgreSQL, TDEngine, CockroachDB, TiDB, ClickHouse
- }
-
public String getUserName() {
return userName;
}
@@ -91,21 +231,30 @@ public String getPassword() {
return password;
}
- public class DBMSConverter implements IStringConverter {
- @Override
- public DBMS convert(String value) {
- return DBMS.valueOf(value);
- }
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
}
public boolean printProgressInformation() {
return printProgressInformation;
}
+ public boolean printProgressSummary() {
+ return printProgressSummary;
+ }
+
public int getTimeoutSeconds() {
return timeoutSeconds;
}
+ public int getMaxGeneratedDatabases() {
+ return maxGeneratedDatabases;
+ }
+
public int getErrorExitCode() {
return errorExitCode;
}
@@ -114,4 +263,84 @@ public long getRandomSeed() {
return randomSeed;
}
+ public boolean testAggregateFunctionsPQS() {
+ return testAggregateFunctions;
+ }
+
+ public boolean testOnlyWithMoreThanZeroRows() {
+ return testOnlyWithMoreThanZeroRows;
+ }
+
+ public StringGenerationStrategy getRandomStringGenerationStrategy() {
+ return randomStringGenerationStrategy;
+ }
+
+ public int getMaxStringConstantLength() {
+ return maxStringConstantLength;
+ }
+
+ public boolean useConstantCaching() {
+ return useConstantCaching;
+ }
+
+ public int getConstantCacheSize() {
+ return constantCacheSize;
+ }
+
+ public boolean isHelp() {
+ return help;
+ }
+
+ public boolean isDefaultPassword() {
+ return Objects.equals(password, DEFAULT_OPTIONS.password);
+ }
+
+ public boolean isDefaultUsername() {
+ return Objects.equals(userName, DEFAULT_OPTIONS.userName);
+ }
+
+ public String getDatabasePrefix() {
+ return databasePrefix;
+ }
+
+ public boolean performConnectionTest() {
+ return useConnectionTest;
+ }
+
+ public boolean serializeReproduceState() {
+ return serializeReproduceState;
+ }
+
+ public boolean useReducer() {
+ return useReducer;
+ }
+
+ public boolean reduceAST() {
+ return reduceAST;
+ }
+
+ public long getMaxStatementReduceSteps() {
+ return maxStatementReduceSteps;
+ }
+
+ public long getMaxStatementReduceTime() {
+ return maxStatementReduceTime;
+ }
+
+ public long getMaxASTReduceSteps() {
+ return maxASTReduceSteps;
+ }
+
+ public long getMaxASTReduceTime() {
+ return maxASTReduceTime;
+ }
+
+ public boolean validateResultSizeOnly() {
+ return validateResultSizeOnly;
+ }
+
+ public boolean canonicalizeSqlString() {
+ return canonicalizeSqlString;
+ }
+
}
diff --git a/src/sqlancer/OracleFactory.java b/src/sqlancer/OracleFactory.java
new file mode 100644
index 000000000..9d6e1704b
--- /dev/null
+++ b/src/sqlancer/OracleFactory.java
@@ -0,0 +1,18 @@
+package sqlancer;
+
+import sqlancer.common.oracle.TestOracle;
+
+public interface OracleFactory> {
+
+ TestOracle create(G globalState) throws Exception;
+
+ /**
+ * Indicates whether the test oracle requires that all tables (including views) contain at least one row.
+ *
+ * @return whether the test oracle requires at least one row per table
+ */
+ default boolean requiresAllTablesToContainRows() {
+ return false;
+ }
+
+}
diff --git a/src/sqlancer/ProviderAdapter.java b/src/sqlancer/ProviderAdapter.java
index fcc076a23..346567300 100644
--- a/src/sqlancer/ProviderAdapter.java
+++ b/src/sqlancer/ProviderAdapter.java
@@ -1,25 +1,39 @@
package sqlancer;
-import java.io.FileWriter;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
-public abstract class ProviderAdapter, O> implements DatabaseProvider {
+import sqlancer.StateToReproduce.OracleRunReproductionState;
+import sqlancer.common.DBMSCommon;
+import sqlancer.common.oracle.CompositeTestOracle;
+import sqlancer.common.oracle.TestOracle;
+import sqlancer.common.schema.AbstractSchema;
+
+public abstract class ProviderAdapter, C>, O extends DBMSSpecificOptions extends OracleFactory>, C extends SQLancerDBConnection>
+ implements DatabaseProvider {
private final Class globalClass;
private final Class optionClass;
- public ProviderAdapter(Class globalClass, Class optionClass) {
+ // Variables for QPG
+ Map queryPlanPool = new HashMap<>();
+ static double[] weightedAverageReward; // static variable for sharing across all threads
+ int currentSelectRewards;
+ int currentSelectCounts;
+ int currentMutationOperator = -1;
+
+ protected ProviderAdapter(Class globalClass, Class optionClass) {
this.globalClass = globalClass;
this.optionClass = optionClass;
}
- @Override
- public void printDatabaseSpecificState(FileWriter writer, StateToReproduce state) {
-
- }
-
@Override
public StateToReproduce getStateToReproduce(String databaseName) {
- return new StateToReproduce(databaseName);
+ return new StateToReproduce(databaseName, this);
}
@Override
@@ -32,4 +46,213 @@ public Class getOptionClass() {
return optionClass;
}
+ @Override
+ public Reproducer generateAndTestDatabase(G globalState) throws Exception {
+ try {
+ generateDatabase(globalState);
+ checkViewsAreValid(globalState);
+ globalState.getManager().incrementCreateDatabase();
+
+ TestOracle oracle = getTestOracle(globalState);
+ for (int i = 0; i < globalState.getOptions().getNrQueries(); i++) {
+ try (OracleRunReproductionState localState = globalState.getState().createLocalState()) {
+ assert localState != null;
+ try {
+ oracle.check();
+ globalState.getManager().incrementSelectQueryCount();
+ } catch (IgnoreMeException ignored) {
+ } catch (AssertionError e) {
+ Reproducer reproducer = oracle.getLastReproducer();
+ if (reproducer != null) {
+ return reproducer;
+ }
+ throw e;
+ }
+ localState.executedWithoutError();
+ }
+ }
+ } finally {
+ globalState.getConnection().close();
+ }
+ return null;
+ }
+
+ protected abstract void checkViewsAreValid(G globalState) throws SQLException;
+
+ protected TestOracle getTestOracle(G globalState) throws Exception {
+ List extends OracleFactory> testOracleFactory = globalState.getDbmsSpecificOptions()
+ .getTestOracleFactory();
+ boolean testOracleRequiresMoreThanZeroRows = testOracleFactory.stream()
+ .anyMatch(OracleFactory::requiresAllTablesToContainRows);
+ boolean userRequiresMoreThanZeroRows = globalState.getOptions().testOnlyWithMoreThanZeroRows();
+ boolean checkZeroRows = testOracleRequiresMoreThanZeroRows || userRequiresMoreThanZeroRows;
+ if (checkZeroRows && globalState.getSchema().containsTableWithZeroRows(globalState)) {
+ if (globalState.getOptions().enableQPG()) {
+ addRowsToAllTables(globalState);
+ } else {
+ throw new IgnoreMeException();
+ }
+ }
+ if (testOracleFactory.size() == 1) {
+ return testOracleFactory.get(0).create(globalState);
+ } else {
+ return new CompositeTestOracle<>(testOracleFactory.stream().map(o -> {
+ try {
+ return o.create(globalState);
+ } catch (Exception e1) {
+ throw new AssertionError(e1);
+ }
+ }).collect(Collectors.toList()), globalState);
+ }
+ }
+
+ public abstract void generateDatabase(G globalState) throws Exception;
+
+ // QPG: entry function
+ @Override
+ public void generateAndTestDatabaseWithQueryPlanGuidance(G globalState) throws Exception {
+ if (weightedAverageReward == null) {
+ weightedAverageReward = initializeWeightedAverageReward(); // Same length as the list of mutators
+ }
+ try {
+ generateDatabase(globalState);
+ checkViewsAreValid(globalState);
+ globalState.getManager().incrementCreateDatabase();
+
+ Long executedQueryCount = 0L;
+ while (executedQueryCount < globalState.getOptions().getNrQueries()) {
+ int numOfNoNewQueryPlans = 0;
+ TestOracle oracle = getTestOracle(globalState);
+ while (executedQueryCount < globalState.getOptions().getNrQueries()) {
+ try (OracleRunReproductionState localState = globalState.getState().createLocalState()) {
+ assert localState != null;
+ try {
+ oracle.check();
+ String query = oracle.getLastQueryString();
+ executedQueryCount += 1;
+ if (addQueryPlan(query, globalState)) {
+ numOfNoNewQueryPlans = 0;
+ } else {
+ numOfNoNewQueryPlans++;
+ }
+ globalState.getManager().incrementSelectQueryCount();
+ } catch (IgnoreMeException e) {
+
+ }
+ localState.executedWithoutError();
+ }
+ // exit loop to mutate tables if no new query plans have been found after a while
+ if (numOfNoNewQueryPlans > globalState.getOptions().getQPGMaxMutationInterval()) {
+ mutateTables(globalState);
+ break;
+ }
+ }
+ }
+ } finally {
+ globalState.getConnection().close();
+ }
+ }
+
+ // QPG: mutate tables for a new database state
+ private synchronized boolean mutateTables(G globalState) throws Exception {
+ // Update rewards based on a set of newly generated queries in last iteration
+ if (currentMutationOperator != -1) {
+ weightedAverageReward[currentMutationOperator] += ((double) currentSelectRewards
+ / (double) currentSelectCounts) * globalState.getOptions().getQPGk();
+ }
+ currentMutationOperator = -1;
+
+ // Choose mutator based on the rewards
+ int selectedActionIndex = 0;
+ if (Randomly.getPercentage() < globalState.getOptions().getQPGProbability()) {
+ selectedActionIndex = globalState.getRandomly().getInteger(0, weightedAverageReward.length);
+ } else {
+ selectedActionIndex = DBMSCommon.getMaxIndexInDoubleArray(weightedAverageReward);
+ }
+ int reward = 0;
+
+ try {
+ executeMutator(selectedActionIndex, globalState);
+ checkViewsAreValid(globalState); // Remove the invalid views
+ reward = checkQueryPlan(globalState);
+ } catch (IgnoreMeException | AssertionError e) {
+ } finally {
+ // Update rewards based on existing queries associated with the query plan pool
+ updateReward(selectedActionIndex, (double) reward / (double) queryPlanPool.size(), globalState);
+ currentMutationOperator = selectedActionIndex;
+ }
+
+ // Clear the variables for storing the rewards of the action on a set of newly generated queries
+ currentSelectRewards = 0;
+ currentSelectCounts = 0;
+ return true;
+ }
+
+ // QPG: add a query plan to the query plan pool and return true if the query plan is new
+ private boolean addQueryPlan(String selectStr, G globalState) throws Exception {
+ String queryPlan = getQueryPlan(selectStr, globalState);
+
+ if (globalState.getOptions().logQueryPlan()) {
+ globalState.getLogger().writeQueryPlan(queryPlan);
+ }
+
+ currentSelectCounts += 1;
+ if (queryPlanPool.containsKey(queryPlan)) {
+ return false;
+ } else {
+ queryPlanPool.put(queryPlan, selectStr);
+ currentSelectRewards += 1;
+ return true;
+ }
+ }
+
+ // Obtain the reward of the current action based on the queries associated with the query plan pool
+ private int checkQueryPlan(G globalState) throws Exception {
+ int newQueryPlanFound = 0;
+ HashMap modifiedQueryPlan = new HashMap<>();
+ for (Iterator> it = queryPlanPool.entrySet().iterator(); it.hasNext();) {
+ Map.Entry item = it.next();
+ String queryPlan = item.getKey();
+ String selectStr = item.getValue();
+ String newQueryPlan = getQueryPlan(selectStr, globalState);
+ if (newQueryPlan.isEmpty()) { // Invalid query
+ it.remove();
+ } else if (!queryPlan.equals(newQueryPlan)) { // A query plan has been changed
+ it.remove();
+ modifiedQueryPlan.put(newQueryPlan, selectStr);
+ if (!queryPlanPool.containsKey(newQueryPlan)) { // A new query plan is found
+ newQueryPlanFound++;
+ }
+ }
+ }
+ queryPlanPool.putAll(modifiedQueryPlan);
+ return newQueryPlanFound;
+ }
+
+ // QPG: update the reward of current action
+ private void updateReward(int actionIndex, double reward, G globalState) {
+ weightedAverageReward[actionIndex] += (reward - weightedAverageReward[actionIndex])
+ * globalState.getOptions().getQPGk();
+ }
+
+ // QPG: initialize the weighted average reward of all mutation operators (required implementation in specific DBMS)
+ protected double[] initializeWeightedAverageReward() {
+ throw new UnsupportedOperationException();
+ }
+
+ // QPG: obtain the query plan of a query (required implementation in specific DBMS)
+ protected String getQueryPlan(String selectStr, G globalState) throws Exception {
+ throw new UnsupportedOperationException();
+ }
+
+ // QPG: execute a mutation operator (required implementation in specific DBMS)
+ protected void executeMutator(int index, G globalState) throws Exception {
+ throw new UnsupportedOperationException();
+ }
+
+ // QPG: add rows to all tables (required implementation in specific DBMS when enabling PQS oracle for QPG)
+ protected boolean addRowsToAllTables(G globalState) throws Exception {
+ throw new UnsupportedOperationException();
+ }
+
}
diff --git a/src/sqlancer/Query.java b/src/sqlancer/Query.java
deleted file mode 100644
index 622f48035..000000000
--- a/src/sqlancer/Query.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package sqlancer;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.Collection;
-
-public abstract class Query {
-
- public abstract String getQueryString();
-
- /**
- * Whether the query could affect the schema (i.e., by add/deleting columns or tables).
- *
- * @return
- */
- public abstract boolean couldAffectSchema();
-
- /**
- *
- * @param con
- *
- * @return true if the query was successful, false otherwise
- *
- * @throws SQLException
- */
- public abstract boolean execute(GlobalState> globalState) throws SQLException;
-
- public abstract Collection getExpectedErrors();
-
- @Override
- public String toString() {
- return getQueryString();
- }
-
- public ResultSet executeAndGet(GlobalState> globalState) throws SQLException {
- throw new AssertionError();
- }
-
- public boolean executeLogged(GlobalState> globalState) throws SQLException {
- logQueryString(globalState);
- return execute(globalState);
- }
-
- public ResultSet executeAndGetLogged(GlobalState> globalState) throws SQLException {
- logQueryString(globalState);
- return executeAndGet(globalState);
- }
-
- private void logQueryString(GlobalState> globalState) {
- if (globalState.getOptions().logEachSelect()) {
- globalState.getLogger().writeCurrent(getQueryString());
- }
- }
-
-}
diff --git a/src/sqlancer/QueryAdapter.java b/src/sqlancer/QueryAdapter.java
deleted file mode 100644
index 0f8cb9ef2..000000000
--- a/src/sqlancer/QueryAdapter.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package sqlancer;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collection;
-
-public class QueryAdapter extends Query {
-
- private final String query;
- private final Collection expectedErrors;
- private final boolean couldAffectSchema;
-
- public QueryAdapter(String query) {
- this(query, new ArrayList<>());
- }
-
- public QueryAdapter(String query, boolean couldAffectSchema) {
- this(query, new ArrayList<>(), couldAffectSchema);
- }
-
- public QueryAdapter(String query, Collection expectedErrors) {
- this.query = query;
- this.expectedErrors = expectedErrors;
- this.couldAffectSchema = false;
- }
-
- public QueryAdapter(String query, Collection expectedErrors, boolean couldAffectSchema) {
- this.query = query;
- this.expectedErrors = expectedErrors;
- this.couldAffectSchema = couldAffectSchema;
- }
-
- @Override
- public String getQueryString() {
- return query;
- }
-
- @Override
- public boolean execute(GlobalState> globalState) throws SQLException {
- try (Statement s = globalState.getConnection().createStatement()) {
- s.execute(query);
- Main.nrSuccessfulActions.addAndGet(1);
- return true;
- } catch (Exception e) {
- Main.nrUnsuccessfulActions.addAndGet(1);
- checkException(e);
- return false;
- }
- }
-
- public void checkException(Exception e) throws AssertionError {
- boolean isExcluded = false;
- for (String expectedError : expectedErrors) {
- if (e.getMessage().contains(expectedError)) {
- isExcluded = true;
- break;
- }
- }
- if (!isExcluded) {
- throw new AssertionError(query, e);
- }
- }
-
- @Override
- public ResultSet executeAndGet(GlobalState> globalState) throws SQLException {
- Statement s = globalState.getConnection().createStatement();
- ResultSet result = null;
- try {
- result = s.executeQuery(query);
- Main.nrSuccessfulActions.addAndGet(1);
- return result;
- } catch (Exception e) {
- s.close();
- boolean isExcluded = false;
- Main.nrUnsuccessfulActions.addAndGet(1);
- for (String expectedError : expectedErrors) {
- if (e.getMessage().contains(expectedError)) {
- isExcluded = true;
- break;
- }
- }
- if (!isExcluded) {
- throw e;
- }
- }
- return null;
- }
-
- @Override
- public boolean couldAffectSchema() {
- return couldAffectSchema;
- }
-
- @Override
- public Collection getExpectedErrors() {
- return expectedErrors;
- }
-
-}
diff --git a/src/sqlancer/QueryProvider.java b/src/sqlancer/QueryProvider.java
deleted file mode 100644
index 99ee67d8c..000000000
--- a/src/sqlancer/QueryProvider.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package sqlancer;
-
-import java.sql.SQLException;
-
-@FunctionalInterface
-public interface QueryProvider {
- Query getQuery(S globalState) throws SQLException;
-}
diff --git a/src/sqlancer/Randomly.java b/src/sqlancer/Randomly.java
index 1488ed43f..8494c189a 100644
--- a/src/sqlancer/Randomly.java
+++ b/src/sqlancer/Randomly.java
@@ -1,54 +1,73 @@
package sqlancer;
import java.math.BigDecimal;
+import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
public final class Randomly {
- private static final boolean USE_CACHING = true;
- private static final int CACHE_SIZE = 100;
+ private static StringGenerationStrategy stringGenerationStrategy = StringGenerationStrategy.SOPHISTICATED;
+ private static int maxStringLength = 10;
+ private static boolean useCaching = true;
+ private static int cacheSize = 100;
private final List cachedLongs = new ArrayList<>();
+ private final List cachedIntegers = new ArrayList<>();
private final List cachedStrings = new ArrayList<>();
private final List cachedDoubles = new ArrayList<>();
private final List cachedBytes = new ArrayList<>();
- private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzöß!#<>/.,~-+'*()[]{} ^*?%_\t\n\r|&\\";
private Supplier provider;
private static final ThreadLocal THREAD_RANDOM = new ThreadLocal<>();
+ private long seed;
private void addToCache(long val) {
- if (USE_CACHING && cachedLongs.size() < CACHE_SIZE && !cachedLongs.contains(val)) {
+ if (useCaching && cachedLongs.size() < cacheSize && !cachedLongs.contains(val)) {
cachedLongs.add(val);
}
}
+ private void addToCache(int val) {
+ if (useCaching && cachedIntegers.size() < cacheSize && !cachedIntegers.contains(val)) {
+ cachedIntegers.add(val);
+ }
+ }
+
private void addToCache(double val) {
- if (USE_CACHING && cachedDoubles.size() < CACHE_SIZE && !cachedDoubles.contains(val)) {
+ if (useCaching && cachedDoubles.size() < cacheSize && !cachedDoubles.contains(val)) {
cachedDoubles.add(val);
}
}
private void addToCache(String val) {
- if (USE_CACHING && cachedStrings.size() < CACHE_SIZE && !cachedStrings.contains(val)) {
+ if (useCaching && cachedStrings.size() < cacheSize && !cachedStrings.contains(val)) {
cachedStrings.add(val);
}
}
private Long getFromLongCache() {
- if (!USE_CACHING || cachedLongs.isEmpty()) {
+ if (!useCaching || cachedLongs.isEmpty()) {
return null;
} else {
return Randomly.fromList(cachedLongs);
}
}
+ private Integer getFromIntegerCache() {
+ if (!useCaching || cachedIntegers.isEmpty()) {
+ return null;
+ } else {
+ return Randomly.fromList(cachedIntegers);
+ }
+ }
+
private Double getFromDoubleCache() {
- if (!USE_CACHING) {
+ if (!useCaching) {
return null;
}
if (Randomly.getBoolean() && !cachedLongs.isEmpty()) {
@@ -61,33 +80,22 @@ private Double getFromDoubleCache() {
}
private String getFromStringCache() {
- if (!USE_CACHING) {
+ if (!useCaching) {
return null;
}
if (Randomly.getBoolean() && !cachedLongs.isEmpty()) {
return String.valueOf(Randomly.fromList(cachedLongs));
} else if (Randomly.getBoolean() && !cachedDoubles.isEmpty()) {
return String.valueOf(Randomly.fromList(cachedDoubles));
- } else if (Randomly.getBoolean() && !cachedBytes.isEmpty()) {
+ } else if (Randomly.getBoolean() && !cachedBytes.isEmpty()
+ && stringGenerationStrategy == StringGenerationStrategy.SOPHISTICATED) {
return new String(Randomly.fromList(cachedBytes));
} else if (!cachedStrings.isEmpty()) {
String randomString = Randomly.fromList(cachedStrings);
if (Randomly.getBoolean()) {
return randomString;
} else {
- if (Randomly.getBoolean()) {
- return randomString.toLowerCase();
- } else if (Randomly.getBoolean()) {
- return randomString.toUpperCase();
- } else {
- char[] chars = randomString.toCharArray();
- if (chars.length != 0) {
- for (int i = 0; i < Randomly.smallNumber(); i++) {
- chars[getInteger(0, chars.length)] = ALPHABET.charAt(getInteger(0, ALPHABET.length()));
- }
- }
- return new String(chars);
- }
+ return stringGenerationStrategy.transformCachedString(this, randomString);
}
} else {
return null;
@@ -95,7 +103,7 @@ private String getFromStringCache() {
}
private static boolean cacheProbability() {
- return USE_CACHING && getNextLong(0, 3) == 1;
+ return useCaching && getNextLong(0, 3) == 1;
}
// CACHING END
@@ -127,6 +135,12 @@ public static List nonEmptySubset(List columns, int nr) {
return extractNrRandomColumns(columns, nr);
}
+ public static List nonEmptySubsetLeast(List columns, int min) {
+ int nr = getNextInt(min, columns.size() + 1);
+ assert nr <= columns.size();
+ return extractNrRandomColumns(columns, nr);
+ }
+
public static List nonEmptySubsetPotentialDuplicates(List columns) {
List arr = new ArrayList<>();
for (int i = 0; i < Randomly.smallNumber() + 1; i++) {
@@ -142,17 +156,12 @@ public static List subset(List columns) {
public static List subset(int nr, @SuppressWarnings("unchecked") T... values) {
List list = new ArrayList<>();
- for (T val : values) {
- list.add(val);
- }
+ Collections.addAll(list, values);
return extractNrRandomColumns(list, nr);
}
public static List subset(@SuppressWarnings("unchecked") T... values) {
- List list = new ArrayList<>();
- for (T val : values) {
- list.add(val);
- }
+ List list = new ArrayList<>(Arrays.asList(values));
return subset(list);
}
@@ -168,13 +177,17 @@ public static List extractNrRandomColumns(List columns, int nr) {
public static int smallNumber() {
// no need to cache for small numbers
- return (int) (Math.abs(getThreadRandom().get().nextGaussian()) * 2);
+ return (int) (Math.abs(getThreadRandom().get().nextGaussian())) * 2;
}
public static boolean getBoolean() {
return getThreadRandom().get().nextBoolean();
}
+ public static double getPercentage() {
+ return getThreadRandom().get().nextDouble();
+ }
+
private static ThreadLocal getThreadRandom() {
if (THREAD_RANDOM.get() == null) {
// a static method has been called, before Randomly was instantiated
@@ -199,59 +212,137 @@ public long getInteger() {
}
}
- public String getString() {
- if (smallBiasProbability()) {
- return Randomly.fromOptions("TRUE", "FALSE", "0.0", "-0.0", "1e500", "-1e500");
- }
- if (cacheProbability()) {
- String s = getFromStringCache();
- if (s != null) {
- return s;
+ public enum StringGenerationStrategy {
+
+ NUMERIC {
+ @Override
+ public String getString(Randomly r) {
+ return getStringOfAlphabet(r, NUMERIC_ALPHABET);
}
- }
- int n = ALPHABET.length();
+ },
+ ALPHANUMERIC {
+ @Override
+ public String getString(Randomly r) {
+ return getStringOfAlphabet(r, ALPHANUMERIC_ALPHABET);
- StringBuilder sb = new StringBuilder();
+ }
- int chars;
- if (Randomly.getBoolean()) {
- chars = Randomly.smallNumber();
- } else {
- chars = getInteger(0, 30);
- }
- for (int i = 0; i < chars; i++) {
- if (Randomly.getBooleanWithRatherLowProbability()) {
- char val = (char) getInteger();
- if (val != 0) {
- sb.append(val);
+ },
+ ALPHANUMERIC_SPECIALCHAR {
+ @Override
+ public String getString(Randomly r) {
+ return getStringOfAlphabet(r, ALPHANUMERIC_SPECIALCHAR_ALPHABET);
+
+ }
+
+ },
+ SOPHISTICATED {
+
+ private static final String ALPHABET = ALPHANUMERIC_SPECIALCHAR_ALPHABET;
+
+ @Override
+ public String getString(Randomly r) {
+ if (smallBiasProbability()) {
+ return Randomly.fromOptions("TRUE", "FALSE", "0.0", "-0.0", "1e500", "-1e500");
}
- } else {
- sb.append(ALPHABET.charAt(getNextInt(0, n)));
+ if (cacheProbability()) {
+ String s = r.getFromStringCache();
+ if (s != null) {
+ return s;
+ }
+ }
+
+ int n = ALPHABET.length();
+
+ StringBuilder sb = new StringBuilder();
+
+ int chars = getStringLength(r);
+ for (int i = 0; i < chars; i++) {
+ if (Randomly.getBooleanWithRatherLowProbability()) {
+ char val = (char) r.getInteger();
+ if (val != 0) {
+ sb.append(val);
+ }
+ } else {
+ sb.append(ALPHABET.charAt(getNextInt(0, n)));
+ }
+ }
+ while (Randomly.getBooleanWithSmallProbability()) {
+ String[][] pairs = { { "{", "}" }, { "[", "]" }, { "(", ")" } };
+ int idx = (int) Randomly.getNotCachedInteger(0, pairs.length);
+ int left = (int) Randomly.getNotCachedInteger(0, sb.length() + 1);
+ sb.insert(left, pairs[idx][0]);
+ int right = (int) Randomly.getNotCachedInteger(left + 1, sb.length() + 1);
+ sb.insert(right, pairs[idx][1]);
+ }
+ if (r.provider != null) {
+ while (Randomly.getBooleanWithSmallProbability()) {
+ if (sb.length() == 0) {
+ sb.append(r.provider.get());
+ } else {
+ sb.insert((int) Randomly.getNotCachedInteger(0, sb.length()), r.provider.get());
+ }
+ }
+ }
+
+ String s = sb.toString();
+
+ r.addToCache(s);
+ return s;
}
- }
- while (Randomly.getBooleanWithSmallProbability()) {
- String[][] pairs = { { "{", "}" }, { "[", "]" }, { "(", ")" } };
- int idx = (int) Randomly.getNotCachedInteger(0, pairs.length);
- int left = (int) Randomly.getNotCachedInteger(0, sb.length() + 1);
- sb.insert(left, pairs[idx][0]);
- int right = (int) Randomly.getNotCachedInteger(left + 1, sb.length() + 1);
- sb.insert(right, pairs[idx][1]);
- }
- if (provider != null) {
- while (Randomly.getBooleanWithSmallProbability()) {
- if (sb.length() == 0) {
- sb.append(provider.get());
+
+ public String transformCachedString(Randomly r, String randomString) {
+ if (Randomly.getBoolean()) {
+ return randomString.toLowerCase();
+ } else if (Randomly.getBoolean()) {
+ return randomString.toUpperCase();
} else {
- sb.insert((int) Randomly.getNotCachedInteger(0, sb.length()), provider.get());
+ char[] chars = randomString.toCharArray();
+ if (chars.length != 0) {
+ for (int i = 0; i < Randomly.smallNumber(); i++) {
+ chars[r.getInteger(0, chars.length)] = ALPHABET.charAt(r.getInteger(0, ALPHABET.length()));
+ }
+ }
+ return new String(chars);
}
}
+
+ };
+
+ private static final String ALPHANUMERIC_SPECIALCHAR_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#<>/.,~-+'*()[]{} ^*?%_\t\n\r|&\\";
+ private static final String ALPHANUMERIC_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ private static final String NUMERIC_ALPHABET = "0123456789";
+
+ private static int getStringLength(Randomly r) {
+ int chars;
+ if (Randomly.getBoolean()) {
+ chars = Randomly.smallNumber();
+ } else {
+ chars = r.getInteger(0, maxStringLength);
+ }
+ return chars;
+ }
+
+ private static String getStringOfAlphabet(Randomly r, String alphabet) {
+ int chars = getStringLength(r);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < chars; i++) {
+ sb.append(alphabet.charAt(getNextInt(0, alphabet.length())));
+ }
+ return sb.toString();
}
- String s = sb.toString();
+ public abstract String getString(Randomly r);
+
+ public String transformCachedString(Randomly r, String s) {
+ return s;
+ }
- addToCache(s);
- return s;
+ }
+
+ public String getString() {
+ return stringGenerationStrategy.getString(this);
}
public byte[] getBytes() {
@@ -275,7 +366,6 @@ public long getNonZeroInteger() {
do {
value = getInteger();
} while (value == 0);
- assert value != 0;
addToCache(value);
return value;
}
@@ -298,6 +388,24 @@ public long getPositiveInteger() {
return value;
}
+ public int getPositiveIntegerInt() {
+ if (cacheProbability()) {
+ Integer value = getFromIntegerCache();
+ if (value != null && value >= 0) {
+ return value;
+ }
+ }
+ int value;
+ if (smallBiasProbability()) {
+ value = Randomly.fromOptions(0, Integer.MAX_VALUE, 1);
+ } else {
+ value = getNextInt(0, Integer.MAX_VALUE);
+ }
+ addToCache(value);
+ assert value >= 0;
+ return value;
+ }
+
public double getFiniteDouble() {
while (true) {
double val = getDouble();
@@ -349,8 +457,19 @@ public long getLong(long left, long right) {
return getNextLong(left, right);
}
+ public BigInteger getBigInteger(BigInteger left, BigInteger right) {
+ if (left.equals(right)) {
+ return left;
+ }
+ BigInteger result = new BigInteger(String.valueOf(getInteger(left.intValue(), right.intValue())));
+ if (result.compareTo(left) < 0 && result.compareTo(right) > 0) {
+ throw new IgnoreMeException();
+ }
+ return result;
+ }
+
public BigDecimal getRandomBigDecimal() {
- return new BigDecimal(getThreadRandom().get().nextDouble());
+ return BigDecimal.valueOf(getThreadRandom().get().nextDouble());
}
public long getPositiveIntegerNotNull() {
@@ -366,10 +485,6 @@ public static long getNonCachedInteger() {
return getThreadRandom().get().nextLong();
}
- public static long getPositiveNonCachedInteger() {
- return getNextLong(1, Long.MAX_VALUE);
- }
-
public static long getPositiveOrZeroNonCachedInteger() {
return getNextLong(0, Long.MAX_VALUE);
}
@@ -383,11 +498,12 @@ public Randomly(Supplier provider) {
}
public Randomly() {
- getThreadRandom().set(new Random());
+ THREAD_RANDOM.set(new Random());
}
public Randomly(long seed) {
- getThreadRandom().set(new Random(seed));
+ this.seed = seed;
+ THREAD_RANDOM.set(new Random(seed));
}
public static double getUncachedDouble() {
@@ -403,6 +519,15 @@ public String getChar() {
}
}
+ public String getAlphabeticChar() {
+ while (true) {
+ String s = getChar();
+ if (Character.isAlphabetic(s.charAt(0))) {
+ return s;
+ }
+ }
+ }
+
// see https://stackoverflow.com/a/2546158
// uniformity does not seem to be important for us
// SQLancer previously used ThreadLocalRandom.current().nextLong(lower, upper)
@@ -413,11 +538,22 @@ private static long getNextLong(long lower, long upper) {
if (lower == upper) {
return lower;
}
- return (long) (getThreadRandom().get().longs(lower, upper).findFirst().getAsLong());
+ return getThreadRandom().get().longs(lower, upper).findFirst().getAsLong();
}
private static int getNextInt(int lower, int upper) {
return (int) getNextLong(lower, upper);
}
+ public long getSeed() {
+ return seed;
+ }
+
+ public static void initialize(MainOptions options) {
+ stringGenerationStrategy = options.getRandomStringGenerationStrategy();
+ maxStringLength = options.getMaxStringConstantLength();
+ useCaching = options.useConstantCaching();
+ cacheSize = options.getConstantCacheSize();
+ }
+
}
diff --git a/src/sqlancer/Reducer.java b/src/sqlancer/Reducer.java
new file mode 100644
index 000000000..0e6589262
--- /dev/null
+++ b/src/sqlancer/Reducer.java
@@ -0,0 +1,7 @@
+package sqlancer;
+
+public interface Reducer> {
+
+ void reduce(G state, Reproducer reproducer, G newGlobalState) throws Exception;
+
+}
diff --git a/src/sqlancer/Reproducer.java b/src/sqlancer/Reproducer.java
new file mode 100644
index 000000000..460cc810d
--- /dev/null
+++ b/src/sqlancer/Reproducer.java
@@ -0,0 +1,15 @@
+package sqlancer;
+
+public interface Reproducer> {
+ boolean bugStillTriggers(G globalState);
+
+ /**
+ * Describes how to trigger the bug on the database set up by the reduced statements (e.g., the oracle queries to
+ * run and the failure to expect), so that the reduced test case is complete without the reproducer object.
+ *
+ * @return a human-readable description, or null if the reproducer does not provide one
+ */
+ default String getBugInformation() {
+ return null;
+ }
+}
diff --git a/src/sqlancer/SQLConnection.java b/src/sqlancer/SQLConnection.java
new file mode 100644
index 000000000..ae56c781f
--- /dev/null
+++ b/src/sqlancer/SQLConnection.java
@@ -0,0 +1,34 @@
+package sqlancer;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class SQLConnection implements SQLancerDBConnection {
+
+ private final Connection connection;
+
+ public SQLConnection(Connection connection) {
+ this.connection = connection;
+ }
+
+ @Override
+ public String getDatabaseVersion() throws SQLException {
+ DatabaseMetaData meta = connection.getMetaData();
+ return meta.getDatabaseProductVersion();
+ }
+
+ @Override
+ public void close() throws SQLException {
+ connection.close();
+ }
+
+ public Statement prepareStatement(String arg) throws SQLException {
+ return connection.prepareStatement(arg);
+ }
+
+ public Statement createStatement() throws SQLException {
+ return connection.createStatement();
+ }
+}
diff --git a/src/sqlancer/SQLGlobalState.java b/src/sqlancer/SQLGlobalState.java
new file mode 100644
index 000000000..534086472
--- /dev/null
+++ b/src/sqlancer/SQLGlobalState.java
@@ -0,0 +1,30 @@
+package sqlancer;
+
+import sqlancer.common.query.Query;
+import sqlancer.common.schema.AbstractSchema;
+
+/**
+ * Represents a global state that is valid for a testing session on a given database.
+ *
+ * @param
+ * the option parameter
+ * @param
+ * the schema parameter
+ */
+public abstract class SQLGlobalState, S extends AbstractSchema, ?>>
+ extends GlobalState {
+
+ @Override
+ protected void executeEpilogue(Query> q, boolean success, ExecutionTimer timer) throws Exception {
+ boolean logExecutionTime = getOptions().logExecutionTime();
+ if (success && getOptions().printSucceedingStatements()) {
+ System.out.println(q.getQueryString());
+ }
+ if (logExecutionTime) {
+ getLogger().writeCurrent(" -- " + timer.end().asString());
+ }
+ if (q.couldAffectSchema()) {
+ updateSchema();
+ }
+ }
+}
diff --git a/src/sqlancer/SQLProviderAdapter.java b/src/sqlancer/SQLProviderAdapter.java
new file mode 100644
index 000000000..efb4fab67
--- /dev/null
+++ b/src/sqlancer/SQLProviderAdapter.java
@@ -0,0 +1,44 @@
+package sqlancer;
+
+import java.util.List;
+
+import sqlancer.common.log.LoggableFactory;
+import sqlancer.common.log.SQLLoggableFactory;
+import sqlancer.common.query.SQLQueryAdapter;
+import sqlancer.common.schema.AbstractSchema;
+import sqlancer.common.schema.AbstractTable;
+
+public abstract class SQLProviderAdapter>, O extends DBMSSpecificOptions extends OracleFactory>>
+ extends ProviderAdapter {
+ protected SQLProviderAdapter(Class globalClass, Class optionClass) {
+ super(globalClass, optionClass);
+ }
+
+ @Override
+ public LoggableFactory getLoggableFactory() {
+ return new SQLLoggableFactory();
+ }
+
+ @Override
+ protected void checkViewsAreValid(G globalState) {
+ List extends AbstractTable, ?, ?>> views = globalState.getSchema().getViews();
+ for (AbstractTable, ?, ?> view : views) {
+ SQLQueryAdapter q = new SQLQueryAdapter("SELECT 1 FROM " + view.getName() + " LIMIT 1");
+ try {
+ if (!q.execute(globalState)) {
+ dropView(globalState, view.getName());
+ }
+ } catch (Throwable t) {
+ dropView(globalState, view.getName());
+ }
+ }
+ }
+
+ private void dropView(G globalState, String viewName) {
+ try {
+ globalState.executeStatement(new SQLQueryAdapter("DROP VIEW " + viewName, true));
+ } catch (Throwable t2) {
+ throw new IgnoreMeException();
+ }
+ }
+}
diff --git a/src/sqlancer/SQLancerDBConnection.java b/src/sqlancer/SQLancerDBConnection.java
new file mode 100644
index 000000000..1724dda6c
--- /dev/null
+++ b/src/sqlancer/SQLancerDBConnection.java
@@ -0,0 +1,6 @@
+package sqlancer;
+
+public interface SQLancerDBConnection extends AutoCloseable {
+
+ String getDatabaseVersion() throws Exception;
+}
diff --git a/src/sqlancer/StandaloneReducer.java b/src/sqlancer/StandaloneReducer.java
new file mode 100644
index 000000000..813160060
--- /dev/null
+++ b/src/sqlancer/StandaloneReducer.java
@@ -0,0 +1,140 @@
+package sqlancer;
+
+import java.io.FileWriter;
+import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+import sqlancer.common.query.Query;
+
+/**
+ * A standalone tool to reduce bug-triggering SQL statements using the delta debugging algorithm.
+ */
+public class StandaloneReducer {
+ private int partitionNum = 2;
+ private final StateToReproduce originalState;
+ private final DatabaseProvider, ?, ?> databaseProvider;
+ private final Path outputPath;
+
+ public StandaloneReducer(Path inputPath, Path outputPath) throws Exception {
+ this.originalState = StateToReproduce.deserialize(inputPath);
+ this.databaseProvider = originalState.getDatabaseProvider();
+ if (this.databaseProvider == null) {
+ throw new IllegalStateException("Failed to get database provider from .ser file");
+ }
+ this.outputPath = outputPath != null ? outputPath
+ : Paths.get(inputPath.toString().replaceAll("\\.ser$", ".sql"));
+ }
+
+ /**
+ * Performs the main reduction algorithm using partition-based delta debugging.
+ *
+ * @return List of reduced SQL statements that still trigger bugs.
+ */
+ public List> reduce() throws Exception {
+ List> queries = new ArrayList<>(originalState.getStatements());
+ if (queries.size() <= 1) {
+ return queries;
+ }
+
+ partitionNum = 2;
+ while (queries.size() >= 2) {
+ boolean changedInThisPass = false;
+ List> result = tryReduction(queries);
+
+ if (result.size() < queries.size()) {
+ queries = result;
+ changedInThisPass = true;
+ }
+
+ if (changedInThisPass) {
+ partitionNum = 2;
+ } else {
+ if (partitionNum >= queries.size()) {
+ break;
+ }
+ partitionNum = Math.min(partitionNum * 2, queries.size());
+ }
+ }
+
+ try (PrintWriter writer = new PrintWriter(new FileWriter(outputPath.toFile()))) {
+ for (Query> query : queries) {
+ writer.println(query.getQueryString());
+ }
+ }
+ System.out.println("Reduction completed successfully! SQL statements written to: " + outputPath.toString());
+ System.out.println("Final size: " + queries.size() + " statements ("
+ + String.format("%.1f", (1.0 - (double) queries.size() / originalState.getStatements().size()) * 100)
+ + "% reduction)");
+
+ return queries;
+ }
+
+ private List> tryReduction(List> queries) throws Exception {
+ int start = 0;
+ int subLength = queries.size() / partitionNum;
+
+ while (start < queries.size()) {
+ List> candidateQueries = new ArrayList<>(queries);
+ int endPoint = Math.min(start + subLength, candidateQueries.size());
+ candidateQueries.subList(start, endPoint).clear();
+
+ if (testExceptionStillExists(candidateQueries)) {
+ return candidateQueries;
+ }
+
+ start += subLength;
+ }
+
+ return queries;
+ }
+
+ // Test if bug still exists with reduced query set
+ @SuppressWarnings("unchecked")
+ private , O extends DBMSSpecificOptions>, C extends SQLancerDBConnection> boolean testExceptionStillExists(
+ List> queries) {
+ try {
+ DatabaseProvider typedProvider = (DatabaseProvider) databaseProvider;
+ G globalState = typedProvider.getGlobalStateClass().getDeclaredConstructor().newInstance();
+
+ try (C connection = typedProvider.createDatabase(globalState)) {
+ globalState.setConnection(connection);
+ for (Query> query : queries) {
+ try {
+ Query typedQuery = (Query) query;
+ typedQuery.execute(globalState);
+ } catch (Throwable e) {
+ // Any exception not declared as an expected error by the query indicates that an (unexpected)
+ // exception still exists
+ return true;
+ }
+ }
+ // No exception occurred
+ return false;
+ }
+ } catch (Throwable e) {
+ return true;
+ }
+ }
+
+ public static void main(String[] args) {
+ try {
+ if (args.length == 0) {
+ System.err.println(
+ "Usage: java -cp target/sqlancer-2.0.0.jar sqlancer.StandaloneReducer [output-file]");
+ System.exit(1);
+ }
+ Path inputPath = Paths.get(args[0]);
+ Path outputPath = args.length > 1 ? Paths.get(args[1]) : null;
+
+ StandaloneReducer reducer = new StandaloneReducer(inputPath, outputPath);
+ reducer.reduce();
+ } catch (Throwable e) {
+ System.err.println("ERROR: " + e.getMessage());
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+}
diff --git a/src/sqlancer/StateToReproduce.java b/src/sqlancer/StateToReproduce.java
index ea52d7bbb..17bb367fd 100644
--- a/src/sqlancer/StateToReproduce.java
+++ b/src/sqlancer/StateToReproduce.java
@@ -1,43 +1,38 @@
package sqlancer;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
-import java.util.Map;
-
-import sqlancer.clickhouse.ClickHouseSchema;
-import sqlancer.clickhouse.ast.ClickHouseConstant;
-import sqlancer.clickhouse.ast.ClickHouseExpression;
-import sqlancer.mysql.MySQLSchema.MySQLColumn;
-import sqlancer.mysql.ast.MySQLConstant;
-import sqlancer.mysql.ast.MySQLExpression;
-import sqlancer.postgres.PostgresSchema.PostgresColumn;
-import sqlancer.postgres.ast.PostgresConstant;
-import sqlancer.postgres.ast.PostgresExpression;
-import sqlancer.sqlite3.ast.SQLite3Constant;
-import sqlancer.sqlite3.ast.SQLite3Expression;
-import sqlancer.sqlite3.schema.SQLite3Schema.SQLite3Column;
-
-public class StateToReproduce {
-
- public final List statements = new ArrayList<>();
- public String queryString;
+
+import sqlancer.common.query.Query;
+
+public class StateToReproduce implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private List> statements = new ArrayList<>();
private final String databaseName;
+ private transient DatabaseProvider, ?, ?> databaseProvider;
+
public String databaseVersion;
protected long seedValue;
- public String values;
-
String exception;
- public String queryTargetedTablesString;
+ public transient OracleRunReproductionState localState;
- public String queryTargetedColumnsString;
-
- public StateToReproduce(String databaseName) {
+ public StateToReproduce(String databaseName, DatabaseProvider, ?, ?> databaseProvider) {
this.databaseName = databaseName;
+ this.databaseProvider = databaseProvider;
}
public String getException() {
@@ -52,101 +47,146 @@ public String getDatabaseVersion() {
return databaseVersion;
}
- public List getStatements() {
- return statements;
+ public DatabaseProvider, ?, ?> getDatabaseProvider() {
+ return databaseProvider;
}
- public String getQueryString() {
- return queryString;
+ /**
+ * Logs the statement string without executing the corresponding statement.
+ *
+ * @param queryString
+ * the query string to be logged
+ */
+ public void logStatement(String queryString) {
+ if (queryString == null) {
+ throw new IllegalArgumentException();
+ }
+ logStatement(databaseProvider.getLoggableFactory().getQueryForStateToReproduce(queryString));
+ }
+
+ /**
+ * Logs the statement without executing it.
+ *
+ * @param query
+ * the query to be logged
+ */
+ public void logStatement(Query> query) {
+ if (query == null) {
+ throw new IllegalArgumentException();
+ }
+ statements.add(query);
+ }
+
+ public List> getStatements() {
+ return Collections.unmodifiableList(statements);
+ }
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ public void commentStatements() {
+ for (int i = 0; i < statements.size(); i++) {
+ Query> statement = statements.get(i);
+ Query> newQuery = databaseProvider.getLoggableFactory().commentOutQuery(statement);
+ statements.set(i, newQuery);
+ }
}
public long getSeedValue() {
return seedValue;
}
- public static class MySQLStateToReproduce extends StateToReproduce {
+ /**
+ * Returns a local state in which a test oracle can save useful information about a single run. If the local state
+ * is closed without indicating access to it, the local statements will be added to the global state.
+ *
+ * @return the local state for logging
+ */
+ public OracleRunReproductionState getLocalState() {
+ return localState;
+ }
- public Map randomRowValues;
+ /**
+ * State information that is logged if the test oracle finds a bug or if an exception is thrown.
+ */
+ public class OracleRunReproductionState implements Closeable {
- public MySQLExpression whereClause;
+ private final List> statements = new ArrayList<>();
- public String queryThatSelectsRow;
+ private boolean success;
- public MySQLStateToReproduce(String databaseName) {
- super(databaseName);
+ public OracleRunReproductionState() {
+ StateToReproduce.this.localState = this;
}
- public Map getRandomRowValues() {
- return randomRowValues;
+ public void executedWithoutError() {
+ this.success = true;
}
- public MySQLExpression getWhereClause() {
- return whereClause;
+ public void log(String s) {
+ statements.add(databaseProvider.getLoggableFactory().getQueryForStateToReproduce(s));
}
- }
-
- public static class SQLite3StateToReproduce extends StateToReproduce {
- public Map randomRowValues;
-
- public SQLite3Expression whereClause;
-
- public SQLite3StateToReproduce(String databaseName) {
- super(databaseName);
+ public List> getStatements() {
+ return Collections.unmodifiableList(statements);
}
- public Map getRandomRowValues() {
- return randomRowValues;
- }
+ @Override
+ public void close() {
+ if (!success) {
+ StateToReproduce.this.statements.addAll(statements);
+ }
- public SQLite3Expression getWhereClause() {
- return whereClause;
}
}
- public static class PostgresStateToReproduce extends StateToReproduce {
-
- public Map randomRowValues;
-
- public PostgresExpression whereClause;
-
- public String queryThatSelectsRow;
-
- public PostgresStateToReproduce(String databaseName) {
- super(databaseName);
- }
+ public OracleRunReproductionState createLocalState() {
+ return new OracleRunReproductionState();
+ }
- public Map getRandomRowValues() {
- return randomRowValues;
+ public void serialize(Path path) {
+ try (ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(path))) {
+ oos.writeObject(this);
+ } catch (IOException e) {
+ throw new AssertionError(e);
}
+ }
- public PostgresExpression getWhereClause() {
- return whereClause;
+ public static StateToReproduce deserialize(Path path) {
+ try (ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(path))) {
+ return (StateToReproduce) ois.readObject();
+ } catch (IOException | ClassNotFoundException e) {
+ throw new AssertionError(e);
}
-
}
- public static class ClickHouseStateToReproduce extends StateToReproduce {
-
- public Map randomRowValues;
-
- public ClickHouseExpression whereClause;
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
- public String queryThatSelectsRow;
-
- public ClickHouseStateToReproduce(String databaseName) {
- super(databaseName);
- }
-
- public Map getRandomRowValues() {
- return randomRowValues;
- }
+ out.writeObject(this.databaseProvider != null ? this.databaseProvider.getDBMSName() : null);
+ }
- public ClickHouseExpression getWhereClause() {
- return whereClause;
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ String dbmsName = (String) in.readObject();
+
+ DatabaseProvider, ?, ?> provider = null;
+ if (dbmsName != null) {
+ List> providers = Main.getDBMSProviders();
+ for (DatabaseProvider, ?, ?> p : providers) {
+ if (p.getDBMSName().equals(dbmsName)) {
+ provider = p;
+ break;
+ }
+ }
}
+ this.databaseProvider = provider;
+ }
+ public void setStatements(List> statements) {
+ this.statements = statements;
}
}
diff --git a/src/sqlancer/StatementExecutor.java b/src/sqlancer/StatementExecutor.java
index caf7c5201..4f8f48b8f 100644
--- a/src/sqlancer/StatementExecutor.java
+++ b/src/sqlancer/StatementExecutor.java
@@ -1,10 +1,11 @@
package sqlancer;
-import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
-public class StatementExecutor, A extends AbstractAction> {
+import sqlancer.common.query.Query;
+
+public class StatementExecutor, A extends AbstractAction> {
private final G globalState;
private final A[] actions;
@@ -13,7 +14,7 @@ public class StatementExecutor, A extends AbstractActio
@FunctionalInterface
public interface AfterQueryAction {
- void notify(Query q) throws SQLException;
+ void notify(Query> q) throws Exception;
}
@FunctionalInterface
@@ -28,7 +29,8 @@ public StatementExecutor(G globalState, A[] actions, ActionMapper mapping,
this.queryConsumer = queryConsumer;
}
- public void executeStatements() throws SQLException {
+ @SuppressWarnings("unchecked")
+ public void executeStatements() throws Exception {
Randomly r = globalState.getRandomly();
int[] nrRemaining = new int[actions.length];
List availableActions = new ArrayList<>();
@@ -58,21 +60,21 @@ public void executeStatements() throws SQLException {
assert nextAction != null;
assert nrRemaining[i] > 0;
nrRemaining[i]--;
+ @SuppressWarnings("rawtypes")
Query query = null;
try {
boolean success;
int nrTries = 0;
do {
query = nextAction.getQuery(globalState);
- if (globalState.getOptions().logEachSelect()) {
- globalState.getLogger().writeCurrent(query.getQueryString());
- }
- success = globalState.getManager().execute(query);
- } while (!success && nrTries++ < globalState.getOptions().getNrStatementRetryCount());
- } catch (IgnoreMeException e) {
+ success = globalState.executeStatement(query);
+ } while (nextAction.canBeRetried() && !success
+ && nrTries++ < globalState.getOptions().getNrStatementRetryCount());
+ } catch (IgnoreMeException ignored) {
}
if (query != null && query.couldAffectSchema()) {
+ globalState.updateSchema();
queryConsumer.notify(query);
}
total--;
diff --git a/src/sqlancer/StatementReducer.java b/src/sqlancer/StatementReducer.java
new file mode 100644
index 000000000..6545fb2af
--- /dev/null
+++ b/src/sqlancer/StatementReducer.java
@@ -0,0 +1,147 @@
+package sqlancer;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import sqlancer.common.query.Query;
+
+public class StatementReducer, O extends DBMSSpecificOptions>, C extends SQLancerDBConnection>
+ implements Reducer {
+ private final DatabaseProvider provider;
+ private boolean observedChange;
+ private int partitionNum;
+
+ private long currentReduceSteps;
+ private long currentReduceTime;
+
+ private long maxReduceSteps;
+ private long maxReduceTime;
+
+ Instant timeOfReductionBegins;
+
+ public StatementReducer(DatabaseProvider provider) {
+ this.provider = provider;
+ }
+
+ private boolean hasNotReachedLimit(long curr, long limit) {
+ if (limit == MainOptions.NO_REDUCE_LIMIT) {
+ return true;
+ }
+ return curr < limit;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void reduce(G state, Reproducer reproducer, G newGlobalState) throws Exception {
+
+ maxReduceTime = state.getOptions().getMaxStatementReduceTime();
+ maxReduceSteps = state.getOptions().getMaxStatementReduceSteps();
+
+ List> knownToReproduceBugStatements = new ArrayList<>();
+ for (Query> stat : state.getState().getStatements()) {
+ knownToReproduceBugStatements.add((Query) stat);
+ }
+
+ // System.out.println("Starting query:");
+ // Main.StateLogger logger = newGlobalState.getLogger();
+ // printQueries(knownToReproduceBugStatements);
+ // System.out.println();
+
+ if (knownToReproduceBugStatements.size() <= 1) {
+ return;
+ }
+
+ timeOfReductionBegins = Instant.now();
+ currentReduceSteps = 0;
+ currentReduceTime = 0;
+ partitionNum = 2;
+
+ while (knownToReproduceBugStatements.size() >= 2 && hasNotReachedLimit(currentReduceSteps, maxReduceSteps)
+ && hasNotReachedLimit(currentReduceTime, maxReduceTime)) {
+ observedChange = false;
+
+ knownToReproduceBugStatements = tryReduction(state, reproducer, newGlobalState,
+ knownToReproduceBugStatements);
+
+ if (!observedChange) {
+ if (partitionNum == knownToReproduceBugStatements.size()) {
+ break;
+ }
+ // increase the search granularity
+ partitionNum = Math.min(partitionNum * 2, knownToReproduceBugStatements.size());
+ }
+ }
+
+ // System.out.println("Reduced query:");
+ // printQueries(knownToReproduceBugStatements);
+ newGlobalState.getState().setStatements(new ArrayList<>(knownToReproduceBugStatements));
+ newGlobalState.getLogger().logReduced(newGlobalState.getState(),
+ "Statement reduction finished; the following statements remain");
+
+ }
+
+ private List> tryReduction(G state, // NOPMD
+ Reproducer reproducer, G newGlobalState, List> knownToReproduceBugStatements) throws Exception {
+
+ List> statements = knownToReproduceBugStatements;
+
+ int start = 0;
+ int subLength = statements.size() / partitionNum;
+ while (start < statements.size()) {
+ // newStatements = candidate[:start] + candidate[start+subLength:]
+ // in other word, remove [start, start+subLength) from candidates
+ try (C con2 = provider.createDatabase(newGlobalState)) {
+ newGlobalState.setConnection(con2);
+ List> candidateStatements = new ArrayList<>(statements);
+ int endPoint = Math.min(start + subLength, candidateStatements.size());
+ candidateStatements.subList(start, endPoint).clear();
+ newGlobalState.getState().setStatements(new ArrayList<>(candidateStatements));
+
+ for (Query s : candidateStatements) {
+ try {
+ s.execute(newGlobalState);
+ } catch (Throwable ignoredException) {
+ // ignore
+ }
+ }
+ try {
+ if (reproducer.bugStillTriggers(newGlobalState)) {
+ observedChange = true;
+ statements = candidateStatements;
+ partitionNum = Math.max(partitionNum - 1, 2);
+ // reproducer.outputHook((SQLite3GlobalState) newGlobalState);
+ newGlobalState.getLogger().logReduced(newGlobalState.getState());
+ break;
+
+ }
+ } catch (Throwable ignoredException) {
+
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ currentReduceSteps++;
+ Instant currentInstant = Instant.now();
+
+ currentReduceTime = Duration.between(timeOfReductionBegins, currentInstant).getSeconds();
+ if (!hasNotReachedLimit(currentReduceSteps, maxReduceSteps)
+ || !hasNotReachedLimit(currentReduceTime, maxReduceTime)) {
+ return statements;
+ }
+ start = start + subLength;
+ }
+ return statements;
+ }
+
+ @SuppressWarnings("unused")
+ private void printQueries(List> statements) {
+ System.out.println("===============================");
+ for (Query> q : statements) {
+ System.out.println(q.getLogString());
+ }
+ System.out.println("===============================");
+ }
+}
diff --git a/src/sqlancer/TestOracle.java b/src/sqlancer/TestOracle.java
deleted file mode 100644
index 40586a0f7..000000000
--- a/src/sqlancer/TestOracle.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package sqlancer;
-
-import java.sql.SQLException;
-
-public interface TestOracle {
-
- void check() throws SQLException;
-
- default boolean onlyWorksForNonEmptyTables() {
- return false;
- }
-
-}
diff --git a/src/sqlancer/ast/newast/NewAliasNode.java b/src/sqlancer/ast/newast/NewAliasNode.java
deleted file mode 100644
index 924533214..000000000
--- a/src/sqlancer/ast/newast/NewAliasNode.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package sqlancer.ast.newast;
-
-public class NewAliasNode implements Node {
-
- private final Node expr;
- private final String alias;
-
- public NewAliasNode(Node expr, String alias) {
- this.expr = expr;
- this.alias = alias;
- }
-
- public Node getExpr() {
- return expr;
- }
-
- public String getAlias() {
- return alias;
- }
-
-}
diff --git a/src/sqlancer/ast/newast/NewBetweenOperatorNode.java b/src/sqlancer/ast/newast/NewBetweenOperatorNode.java
deleted file mode 100644
index 91c54a5f1..000000000
--- a/src/sqlancer/ast/newast/NewBetweenOperatorNode.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package sqlancer.ast.newast;
-
-public class NewBetweenOperatorNode implements Node {
-
- protected Node left;
- protected Node middle;
- protected Node right;
- protected boolean isTrue;
-
- public NewBetweenOperatorNode(Node left, Node middle, Node right, boolean isTrue) {
- this.left = left;
- this.middle = middle;
- this.right = right;
- this.isTrue = isTrue;
- }
-
- public Node getLeft() {
- return left;
- }
-
- public Node getMiddle() {
- return middle;
- }
-
- public Node getRight() {
- return right;
- }
-
- public boolean isTrue() {
- return isTrue;
- }
-
-}
diff --git a/src/sqlancer/ast/newast/NewBinaryOperatorNode.java b/src/sqlancer/ast/newast/NewBinaryOperatorNode.java
deleted file mode 100644
index 3e703e8bd..000000000
--- a/src/sqlancer/ast/newast/NewBinaryOperatorNode.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package sqlancer.ast.newast;
-
-import sqlancer.ast.BinaryOperatorNode.Operator;
-
-public class NewBinaryOperatorNode implements Node {
-
- protected final Operator op;
- protected final Node