diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5c53192aa..9f0dbba32 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,11 +24,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -36,7 +34,7 @@ jobs: - name: Verify run: mvn -B verify -DskipTests=true - name: Misc Tests - run: mvn -B '-Dtest=!sqlancer.dbms.**,!sqlancer.qpg.**,!sqlancer.reducer.**' test + run: mvn -Djacoco.skip=true -B '-Dtest=!sqlancer.dbms.**,!sqlancer.qpg.**' test - name: Set up Python uses: actions/setup-python@v4 with: @@ -48,11 +46,9 @@ jobs: name: DBMS Tests (Citus) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -63,9 +59,11 @@ jobs: 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 apt-get -y install postgresql-15-citus-11.1 + 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/15/bin:$PATH + export PATH=/usr/lib/postgresql/17/bin:$PATH cd ~ mkdir -p citus/coordinator citus/worker1 citus/worker2 initdb -D citus/coordinator @@ -90,43 +88,15 @@ jobs: 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 -Dtest=TestCitus test - - cnosdb: - name: DBMS Tests (CnosDB) - runs-on: ubuntu-latest - 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: Set up ClickHouse - run: | - docker pull cnosdb/cnosdb:community-latest - docker run --name cnosdb -p 8902:8902 -d cnosdb/cnosdb:community-latest - sleep 5 - - name: Run Tests - run: | - CNOSDB_AVAILABLE=true mvn -Dtest=TestCnosDBNoREC test - sleep 20 - CNOSDB_AVAILABLE=true mvn -Dtest=TestCnosDBTLP test + run: CITUS_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestCitus test clickhouse: name: DBMS Tests (ClickHouse) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -137,9 +107,9 @@ jobs: 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 - sleep 5 + 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 -Dtest=ClickHouseBinaryComparisonOperationTest,TestClickHouse,ClickHouseOperatorsVisitorTest,ClickHouseToStringVisitorTest test + 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 @@ -151,11 +121,9 @@ jobs: name: DBMS Tests (CockroachDB) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -166,24 +134,22 @@ jobs: 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 & - sleep 10 + 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 -Dtest=TestCockroachDBNoREC test - COCKROACHDB_AVAILABLE=true mvn -Dtest=TestCockroachDBTLP test - COCKROACHDB_AVAILABLE=true mvn -Dtest=TestCockroachDBCERT test + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -194,18 +160,18 @@ jobs: 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 & - sleep 10 + 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 -Dtest=TestCockroachDBQPG test + 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.687-nightly + image: datafuselabs/databend:v1.2.900-nightly env: QUERY_DEFAULT_USER: sqlancer QUERY_DEFAULT_PASSWORD: sqlancer @@ -213,11 +179,9 @@ jobs: - 8000:8000 - 3307:3307 steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -226,48 +190,64 @@ jobs: run: mvn -B package -DskipTests=true - name: Run Tests run: | - DATABEND_AVAILABLE=true mvn -Dtest=TestDatabendTLP test - DATABEND_AVAILABLE=true mvn -Dtest=TestDatabendNoREC test - DATABEND_AVAILABLE=true mvn -Dtest=TestDatabendPQS test + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up Rust uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - - name: Run DataFusion Server + - 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 run & sleep 300 + 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@v3 + 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 test -Pdatafusion-tests + DATAFUSION_AVAILABLE=true mvn -Djacoco.skip=true test -Pdatafusion-tests duckdb: name: DBMS Tests (DuckDB) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -276,18 +256,16 @@ jobs: run: mvn -B package -DskipTests=true - name: DuckDB Tests run: | - mvn -Dtest=TestDuckDBTLP test - mvn -Dtest=TestDuckDBNoREC test + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -295,7 +273,7 @@ jobs: - name: Build SQLancer run: mvn -B package -DskipTests=true - name: Run Tests - run: mvn -Dtest=TestH2 test + run: mvn -Djacoco.skip=true -Dtest=TestH2 test hive: name: DBMS Tests (Hive) @@ -318,30 +296,64 @@ jobs: - 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: HIVE_AVAILABLE=true mvn -Dtest=TestHiveTLP test + run: SPARK_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestSparkTLP test hsqldb: - name: DBMS Tests (HSQLB) + name: DBMS Tests (HSQLDB) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -350,8 +362,8 @@ jobs: run: mvn -B package -DskipTests=true - name: Run Tests run: | - mvn -Dtest=TestHSQLDBNoREC test - mvn -Dtest=TestHSQLDBTLP test + mvn -Djacoco.skip=true -Dtest=TestHSQLDBNoREC test + mvn -Djacoco.skip=true -Dtest=TestHSQLDBTLP test mariadb: name: DBMS Tests (MariaDB) @@ -366,11 +378,9 @@ jobs: options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=10 steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -380,24 +390,20 @@ jobs: - 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 -Dtest=TestMariaDB test + run: MARIADB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestMariaDB test materialize: name: DBMS Tests (Materialize) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up Materialize run: | docker pull materialize/materialized:latest - docker run -d -p6875:6875 -p6877:6877 -p 26257:26257 materialize/materialized:latest - sleep 5 - # Workaround for https://github.com/cockroachdb/cockroach/issues/93892 - psql postgres://root@localhost:26257 -c "SET CLUSTER SETTING sql.stats.forecasts.enabled = false" + 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@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -406,26 +412,22 @@ jobs: run: mvn -B package -DskipTests=true - name: Run Tests run: | - MATERIALIZE_AVAILABLE=true mvn test -Dtest=TestMaterializeNoREC - MATERIALIZE_AVAILABLE=true mvn test -Dtest=TestMaterializeTLP - MATERIALIZE_AVAILABLE=true mvn test -Dtest=TestMaterializePQS + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up Materialize run: | docker pull materialize/materialized:latest - docker run -d -p6875:6875 -p6877:6877 -p 26257:26257 materialize/materialized:latest - sleep 5 - # Workaround for https://github.com/cockroachdb/cockroach/issues/93892 - psql postgres://root@localhost:26257 -c "SET CLUSTER SETTING sql.stats.forecasts.enabled = false" + 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@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -434,26 +436,24 @@ jobs: run: mvn -B package -DskipTests=true - name: Run Tests run: | - MATERIALIZE_AVAILABLE=true mvn test -Dtest=TestMaterializeQPG - MATERIALIZE_AVAILABLE=true mvn test -Dtest=TestMaterializeQueryPlan + 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) + name: DBMS Tests (MySQL, CERT creation only) runs-on: ubuntu-latest services: mysql: - image: mysql:8.0.36 + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -464,20 +464,18 @@ jobs: 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 test -Dtest=TestMySQLPQS - MYSQL_AVAILABLE=true mvn test -Dtest=TestMySQLTLP - MYSQL_AVAILABLE=true mvn test -Dtest=TestMySQLCERT - MYSQL_AVAILABLE=true mvn test -Dtest=TestMySQLDQE + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -487,29 +485,27 @@ jobs: - name: Set up OceanBase run: | docker run -p 2881:2881 --name oceanbase-ce -e MODE=mini -d oceanbase/oceanbase-ce:4.2.1-lts - sleep 120 + 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 test -Dtest=TestOceanBaseNoREC - OCEANBASE_AVAILABLE=true mvn test -Dtest=TestOceanBasePQS - OCEANBASE_AVAILABLE=true mvn test -Dtest=TestOceanBaseTLP + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up PostgreSQL uses: harmon758/postgresql-action@v1.0.0 with: - postgresql version: '13' + postgresql version: '18' postgresql user: 'sqlancer' postgresql password: 'sqlancer' postgresql db: 'test' - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -518,20 +514,18 @@ jobs: run: mvn -B package -DskipTests=true - name: Run Tests run: | - POSTGRES_AVAILABLE=true mvn -Dtest=TestPostgresPQS test - POSTGRES_AVAILABLE=true mvn -Dtest=TestPostgresTLP test - POSTGRES_AVAILABLE=true mvn -Dtest=TestPostgresNoREC test - POSTGRES_AVAILABLE=true mvn -Dtest=TestPostgresCERT test + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -541,24 +535,22 @@ jobs: 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 - sleep 30 + 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 -Dtest=TestPrestoNoREC test - docker restart presto && sleep 30 - PRESTO_AVAILABLE=true mvn -Dtest=TestPrestoTLP test + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -567,21 +559,19 @@ jobs: run: mvn -B package -DskipTests=true - name: SQLite Tests run: | - mvn -Dtest=TestSQLitePQS test - mvn -Dtest=TestSQLiteTLP test - mvn -Dtest=TestSQLiteNoREC test - mvn -Dtest=TestSQLiteCODDTest test + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -590,17 +580,15 @@ jobs: run: mvn -B package -DskipTests=true - name: SQLite Tests for QPG run: | - mvn -Dtest=TestSQLiteQPG test + mvn -Djacoco.skip=true -Dtest=TestSQLiteQPG test tidb: - name: DBMS Tests (TiDB) + name: DBMS Tests (TiDB, TLP creation only) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -611,23 +599,21 @@ jobs: 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 - sleep 10 + 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: 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;" + 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 -Dtest=TestTiDBTLP test - TIDB_AVAILABLE=true mvn -Dtest=TestTiDBCERT test + 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@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -638,21 +624,19 @@ jobs: 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 - sleep 10 + 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: 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;" + 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 -Dtest=TestTiDBQPG test + run: TIDB_AVAILABLE=true mvn -Djacoco.skip=true -Dtest=TestTiDBQPG test yugabyte: name: DBMS Tests (YugabyteDB) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -662,24 +646,23 @@ jobs: - 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 - sleep 5 + 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 -Dtest=TestYSQLNoREC test - YUGABYTE_AVAILABLE=true mvn -Dtest=TestYSQLTLP test - YUGABYTE_AVAILABLE=true mvn -Dtest=TestYSQLPQS test - YUGABYTE_AVAILABLE=true mvn -Dtest=TestYCQL test + 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@v2 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -688,10 +671,15 @@ jobs: 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 - wget -q https://apache-doris-releases.oss-accelerate.aliyuncs.com/apache-doris-2.1.4-bin-x64.tar.gz + [ -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 @@ -700,7 +688,7 @@ jobs: cd ../be ./bin/start_be.sh --daemon - sleep 30 + 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;" @@ -708,26 +696,6 @@ jobs: run: mvn -B package -DskipTests=true - name: Run Tests run: | - DORIS_AVAILABLE=true mvn -Dtest=TestDorisNoREC test - DORIS_AVAILABLE=true mvn -Dtest=TestDorisPQS test - DORIS_AVAILABLE=true mvn -Dtest=TestDorisTLP test - - reducer: - name: Reducer Tests - runs-on: ubuntu-latest - - 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 - run: mvn -B package -DskipTests=true - - name: Run Tests - run: | - mvn -Dtest=TestStatementReducer test + 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/CONTRIBUTING.md b/CONTRIBUTING.md index 5db3bdc69..76b8833ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ If you do not find an option to import Maven projects, you might need to install ## 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`). +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. @@ -66,7 +66,6 @@ Since SQL dialects differ widely, each DBMS to be tested requires a separate imp | YugabyteDB | Working | Typed (YSQL), Untyped (YCQL) | YSQL implementation based on Postgres code. YCQL implementation is primitive for now and uses Cassandra JDBC driver as a proxy interface. | | Databend | Working | Typed | | | QuestDB | Working | Untyped, Generic | The implementation of QuestDB is still WIP, current version covers very basic data types, operations and SQL keywords. | -| CnosDB | Working | Typed | The implementation of CnosDB currently uses Restful API. | | Materialize | Working | Typed | | | Apache Doris | Preliminary | Typed | This is a preliminary implementation, which only contains the common logic of Doris. We have found some errors through it, and hope to improve it in the future. | | Presto | Preliminary | Typed | This is a preliminary implementation, only basic types supported. | @@ -82,6 +81,7 @@ Some DBMS were once supported but subsequently removed. | 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 diff --git a/README.md b/README.md index 134f47666..f41e32d3c 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Usage: SQLancer [options] [command] [command options] **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. -**Supported DBMSs.** SQLancer requires DBMS-specific code for each DBMS that it supports. As of January 2025, it provides support for Citus, ClickHouse, CnosDB, 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). +**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). # Approaches and Papers diff --git a/pom.xml b/pom.xml index 2037b71ce..c4bc71f82 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.12 @@ -302,9 +302,9 @@ 3.49.1.0 - mysql - mysql-connector-java - 8.0.30 + com.mysql + mysql-connector-j + 9.7.0 org.mariadb.jdbc @@ -329,7 +329,7 @@ org.slf4j - slf4j-simple + slf4j-simple 2.0.6 @@ -360,7 +360,7 @@ org.hsqldb hsqldb - 2.7.1 + 2.7.4 runtime @@ -381,17 +381,55 @@ org.apache.hive hive-jdbc - 4.0.1 + 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 diff --git a/src/check_names.py b/src/check_names.py index f2ab346c6..453580f88 100644 --- a/src/check_names.py +++ b/src/check_names.py @@ -35,7 +35,6 @@ def verify_all_dbs(name_to_files: dict[str:List[str]]): 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["CnosDB"] = get_java_files(os.path.join(cwd, "src", "sqlancer", "cnosdb")) 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")) diff --git a/src/sqlancer/ASTBasedReducer.java b/src/sqlancer/ASTBasedReducer.java index 876a2da12..f9468af76 100644 --- a/src/sqlancer/ASTBasedReducer.java +++ b/src/sqlancer/ASTBasedReducer.java @@ -103,8 +103,7 @@ public void reduce(G state, Reproducer reproducer, G newGlobalState) throws E }); if (!initFlag) { - newGlobalState.getLogger() - .logReducer("warning: failed parsing the statement at transformer : " + t); + System.out.println("Error when parsing the statement at transformer :" + t); continue; } t.apply(); @@ -114,7 +113,8 @@ public void reduce(G state, Reproducer reproducer, G newGlobalState) throws E } while (observeChange); newGlobalState.getState().setStatements(new ArrayList<>(reducedStatements)); - newGlobalState.getLogger().logReduced(newGlobalState.getState()); + newGlobalState.getLogger().logReduced(newGlobalState.getState(), + "AST-based reduction finished; the following statements remain"); } public boolean bugStillTriggers() throws Exception { diff --git a/src/sqlancer/ComparatorHelper.java b/src/sqlancer/ComparatorHelper.java index 5da635de2..cee290924 100644 --- a/src/sqlancer/ComparatorHelper.java +++ b/src/sqlancer/ComparatorHelper.java @@ -70,11 +70,12 @@ public static List getResultSetFirstColumnAsString(String queryString, E throw e; } - if (e.getMessage() == null) { - throw new AssertionError(queryString, e); - } - if (errors.errorIsExpected(e.getMessage())) { - throw new IgnoreMeException(); + 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 { diff --git a/src/sqlancer/Main.java b/src/sqlancer/Main.java index 1f2642f95..47ba2aedf 100644 --- a/src/sqlancer/Main.java +++ b/src/sqlancer/Main.java @@ -26,7 +26,6 @@ import sqlancer.citus.CitusProvider; import sqlancer.clickhouse.ClickHouseProvider; -import sqlancer.cnosdb.CnosDBProvider; import sqlancer.cockroachdb.CockroachDBProvider; import sqlancer.common.log.Loggable; import sqlancer.common.query.Query; @@ -44,6 +43,7 @@ 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; @@ -80,6 +80,9 @@ public static final class StateLogger { 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; @@ -262,33 +265,32 @@ public void writeQueryPlan(String queryPlan) { } } - public void logReducer(String reducerLog) { - FileWriter reduceFileWriter = getReduceFileWriter(); - - StringBuilder sb = new StringBuilder(); - sb.append("[reducer log] "); - sb.append(reducerLog); - 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(); - } - } + 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(); - for (Query s : state.getStatements()) { - sb.append(databaseProvider.getLoggableFactory().createLoggable(s.getLogString()).getLogString()); + 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()); @@ -305,6 +307,12 @@ public void logReduced(StateToReproduce state) { } + 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) { Loggable stackTrace = getStackTrace(reduce); FileWriter logFileWriter2 = getLogFileWriter(); @@ -460,6 +468,9 @@ public void run() throws Exception { 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); @@ -484,6 +495,17 @@ public void run() throws Exception { 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); @@ -493,6 +515,7 @@ public void run() throws Exception { 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); @@ -502,11 +525,28 @@ public void run() throws Exception { astBasedReducer.reduce(state, reproducer, newGlobalState); } - try { - logger.getReduceFileWriter().close(); - logger.reduceFileWriter = null; - } catch (IOException e) { - throw new AssertionError(e); + // 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."); @@ -749,13 +789,13 @@ private static void checkForIssue799(List> providers) "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 CnosDBProvider()); 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()); diff --git a/src/sqlancer/MainOptions.java b/src/sqlancer/MainOptions.java index a5142fcf0..25b769312 100644 --- a/src/sqlancer/MainOptions.java +++ b/src/sqlancer/MainOptions.java @@ -126,10 +126,10 @@ public class MainOptions { @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") + @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") + @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") diff --git a/src/sqlancer/Reproducer.java b/src/sqlancer/Reproducer.java index ef64bd0fe..460cc810d 100644 --- a/src/sqlancer/Reproducer.java +++ b/src/sqlancer/Reproducer.java @@ -2,4 +2,14 @@ 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/StateToReproduce.java b/src/sqlancer/StateToReproduce.java index e44d0ccf6..17bb367fd 100644 --- a/src/sqlancer/StateToReproduce.java +++ b/src/sqlancer/StateToReproduce.java @@ -128,6 +128,10 @@ public void log(String s) { statements.add(databaseProvider.getLoggableFactory().getQueryForStateToReproduce(s)); } + public List> getStatements() { + return Collections.unmodifiableList(statements); + } + @Override public void close() { if (!success) { diff --git a/src/sqlancer/StatementReducer.java b/src/sqlancer/StatementReducer.java index e066aca84..6545fb2af 100644 --- a/src/sqlancer/StatementReducer.java +++ b/src/sqlancer/StatementReducer.java @@ -77,7 +77,8 @@ && hasNotReachedLimit(currentReduceTime, maxReduceTime)) { // System.out.println("Reduced query:"); // printQueries(knownToReproduceBugStatements); newGlobalState.getState().setStatements(new ArrayList<>(knownToReproduceBugStatements)); - newGlobalState.getLogger().logReduced(newGlobalState.getState()); + newGlobalState.getLogger().logReduced(newGlobalState.getState(), + "Statement reduction finished; the following statements remain"); } diff --git a/src/sqlancer/citus/CitusBugs.java b/src/sqlancer/citus/CitusBugs.java index a6f4910e6..1fda3be6a 100644 --- a/src/sqlancer/citus/CitusBugs.java +++ b/src/sqlancer/citus/CitusBugs.java @@ -33,6 +33,9 @@ public final class CitusBugs { // https://github.com/citusdata/citus/issues/6298 public static boolean bug6298 = true; + // https://github.com/citusdata/citus/issues/8553 + public static boolean bug8553 = true; + private CitusBugs() { } diff --git a/src/sqlancer/citus/CitusProvider.java b/src/sqlancer/citus/CitusProvider.java index 747f72216..a11424f18 100644 --- a/src/sqlancer/citus/CitusProvider.java +++ b/src/sqlancer/citus/CitusProvider.java @@ -21,11 +21,15 @@ import sqlancer.citus.gen.CitusAlterTableGenerator; import sqlancer.citus.gen.CitusCommon; import sqlancer.citus.gen.CitusDeleteGenerator; +import sqlancer.citus.gen.CitusDiscardGenerator; import sqlancer.citus.gen.CitusIndexGenerator; import sqlancer.citus.gen.CitusInsertGenerator; +import sqlancer.citus.gen.CitusReindexGenerator; import sqlancer.citus.gen.CitusSetGenerator; import sqlancer.citus.gen.CitusTableGenerator; +import sqlancer.citus.gen.CitusTruncateGenerator; import sqlancer.citus.gen.CitusUpdateGenerator; +import sqlancer.citus.gen.CitusVacuumGenerator; import sqlancer.citus.gen.CitusViewGenerator; import sqlancer.common.DBMSCommon; import sqlancer.common.oracle.CompositeTestOracle; @@ -44,15 +48,11 @@ import sqlancer.postgres.gen.PostgresAnalyzeGenerator; import sqlancer.postgres.gen.PostgresClusterGenerator; import sqlancer.postgres.gen.PostgresCommentGenerator; -import sqlancer.postgres.gen.PostgresDiscardGenerator; import sqlancer.postgres.gen.PostgresDropIndexGenerator; import sqlancer.postgres.gen.PostgresNotifyGenerator; -import sqlancer.postgres.gen.PostgresReindexGenerator; import sqlancer.postgres.gen.PostgresSequenceGenerator; import sqlancer.postgres.gen.PostgresStatisticsGenerator; import sqlancer.postgres.gen.PostgresTransactionGenerator; -import sqlancer.postgres.gen.PostgresTruncateGenerator; -import sqlancer.postgres.gen.PostgresVacuumGenerator; @AutoService(DatabaseProvider.class) public class CitusProvider extends PostgresProvider { @@ -82,13 +82,13 @@ public enum Action implements AbstractAction { CREATE_STATISTICS(PostgresStatisticsGenerator::insert), // DROP_STATISTICS(PostgresStatisticsGenerator::remove), // DELETE(CitusDeleteGenerator::create), // - DISCARD(PostgresDiscardGenerator::create), // + DISCARD(CitusDiscardGenerator::create), // DROP_INDEX(PostgresDropIndexGenerator::create), // INSERT(CitusInsertGenerator::insert), // UPDATE(CitusUpdateGenerator::create), // - TRUNCATE(PostgresTruncateGenerator::create), // - VACUUM(PostgresVacuumGenerator::create), // - REINDEX(PostgresReindexGenerator::create), // + TRUNCATE(CitusTruncateGenerator::create), // + VACUUM(CitusVacuumGenerator::create), // + REINDEX(CitusReindexGenerator::create), // SET(CitusSetGenerator::create), // CREATE_INDEX(CitusIndexGenerator::generate), // SET_CONSTRAINTS((g) -> { diff --git a/src/sqlancer/citus/CitusSchema.java b/src/sqlancer/citus/CitusSchema.java index 560191c05..b2550bdce 100644 --- a/src/sqlancer/citus/CitusSchema.java +++ b/src/sqlancer/citus/CitusSchema.java @@ -63,8 +63,8 @@ public static CitusSchema fromConnection(SQLConnection con, String databaseName) "SELECT table_name, column_to_column_name(logicalrelid, partkey) AS dist_col_name, colocationid FROM information_schema.tables LEFT OUTER JOIN pg_dist_partition ON logicalrelid=table_name::regclass WHERE table_schema='public' OR table_schema LIKE 'pg_temp_%';")) { while (rs.next()) { String tableName = rs.getString("table_name"); - /* citus_tables is a helper view, we don't need to test with it so we let's ignore it */ - if (tableName.equals("citus_tables")) { + /* skip Citus-managed views in the public schema (citus_tables, citus_schemas, etc.) */ + if (tableName.startsWith("citus_")) { continue; } String distributionColumnName = rs.getString("dist_col_name"); diff --git a/src/sqlancer/citus/gen/CitusCommon.java b/src/sqlancer/citus/gen/CitusCommon.java index 7a9f9c659..58b1b7c16 100644 --- a/src/sqlancer/citus/gen/CitusCommon.java +++ b/src/sqlancer/citus/gen/CitusCommon.java @@ -18,15 +18,17 @@ public static List getCitusErrors() { errors.add("cannot perform an INSERT without a partition column value"); errors.add("cannot perform an INSERT with NULL in the partition column"); errors.add("recursive CTEs are not supported in distributed queries"); + errors.add("recursive CTEs are only supported when they contain a filter on the distribution column"); errors.add("could not run distributed query with GROUPING SETS, CUBE, or ROLLUP"); errors.add("Subqueries in HAVING cannot refer to outer query"); errors.add("non-IMMUTABLE functions are not allowed in the RETURNING clause"); errors.add("functions used in UPDATE queries on distributed tables must not be VOLATILE"); errors.add("STABLE functions used in UPDATE queries cannot be called with column references"); - errors.add( - "functions used in the WHERE clause of modification queries on distributed tables must not be VOLATILE"); + errors.add("of modification queries on distributed tables must not be VOLATILE"); errors.add("cannot execute ADD CONSTRAINT command with other subcommands"); errors.add("cannot execute ALTER TABLE command involving partition column"); + errors.add("alter table command is currently unsupported"); + errors.add("on distributed partitioned tables are not supported"); errors.add("could not run distributed query with FOR UPDATE/SHARE commands"); errors.add("is not a regular, foreign or partitioned table"); errors.add("must be a distributed table or a reference table"); @@ -53,13 +55,15 @@ public static List getCitusErrors() { errors.add("direct joins between distributed and local tables are not supported"); errors.add("unlogged columnar tables are not supported"); errors.add("UPDATE and CTID scans not supported for ColumnarScan"); - errors.add("indexes not supported for columnar tables"); + errors.add("unsupported access method for the index on columnar table"); + errors.add("BRIN indexes on columnar tables are not supported"); errors.add("invalid byte sequence for encoding \"UTF8\": 0x00"); errors.add("columnar_tuple_insert_speculative not implemented"); errors.add("row field count is 1, expected 2"); errors.add("incorrect binary data format"); errors.add("invalid sign in external \"numeric\" value"); errors.add("Foreign keys and AFTER ROW triggers are not supported for columnar tables"); + errors.addAll(getColumnarOidErrors()); // current errors in Citus (to be removed once fixed) if (CitusBugs.bug3957) { @@ -87,6 +91,19 @@ public static List getCitusErrors() { return errors; } + /** + * Citus can fail with "could not open relation with OID 0" when operating on columnar temporary tables (e.g., USING + * columnar ON COMMIT DROP), during VACUUM, DISCARD TEMPORARY, or INSERT operations where Citus cannot resolve the + * relation OID. + * + * @return the list of expected error substrings for columnar OID resolution failures. + */ + public static List getColumnarOidErrors() { + List errors = new ArrayList<>(); + errors.add("could not open relation with OID 0"); + return errors; + } + public static void addCitusErrors(ExpectedErrors errors) { errors.addAll(getCitusErrors()); } diff --git a/src/sqlancer/citus/gen/CitusDiscardGenerator.java b/src/sqlancer/citus/gen/CitusDiscardGenerator.java new file mode 100644 index 000000000..f4a1b3240 --- /dev/null +++ b/src/sqlancer/citus/gen/CitusDiscardGenerator.java @@ -0,0 +1,20 @@ +package sqlancer.citus.gen; + +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.postgres.PostgresGlobalState; +import sqlancer.postgres.gen.PostgresDiscardGenerator; + +public final class CitusDiscardGenerator { + + private CitusDiscardGenerator() { + } + + public static SQLQueryAdapter create(PostgresGlobalState globalState) { + SQLQueryAdapter discardQuery = PostgresDiscardGenerator.create(globalState); + ExpectedErrors errors = discardQuery.getExpectedErrors(); + CitusCommon.addCitusErrors(errors); + return discardQuery; + } + +} diff --git a/src/sqlancer/citus/gen/CitusReindexGenerator.java b/src/sqlancer/citus/gen/CitusReindexGenerator.java new file mode 100644 index 000000000..6f37cbe06 --- /dev/null +++ b/src/sqlancer/citus/gen/CitusReindexGenerator.java @@ -0,0 +1,20 @@ +package sqlancer.citus.gen; + +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.postgres.PostgresGlobalState; +import sqlancer.postgres.gen.PostgresReindexGenerator; + +public final class CitusReindexGenerator { + + private CitusReindexGenerator() { + } + + public static SQLQueryAdapter create(PostgresGlobalState globalState) { + SQLQueryAdapter reindexQuery = PostgresReindexGenerator.create(globalState); + ExpectedErrors errors = reindexQuery.getExpectedErrors(); + CitusCommon.addCitusErrors(errors); + return reindexQuery; + } + +} diff --git a/src/sqlancer/citus/gen/CitusTableGenerator.java b/src/sqlancer/citus/gen/CitusTableGenerator.java index 86e5d40cf..5d6b8b249 100644 --- a/src/sqlancer/citus/gen/CitusTableGenerator.java +++ b/src/sqlancer/citus/gen/CitusTableGenerator.java @@ -1,5 +1,6 @@ package sqlancer.citus.gen; +import sqlancer.citus.CitusBugs; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.postgres.PostgresGlobalState; import sqlancer.postgres.PostgresSchema; @@ -20,4 +21,12 @@ public static SQLQueryAdapter generate(String tableName, PostgresSchema newSchem return new CitusTableGenerator(tableName, newSchema, generateOnlyKnown, globalState).generate(); } + @Override + protected void generateInherits() { + if (CitusBugs.bug8553) { + return; + } + super.generateInherits(); + } + } diff --git a/src/sqlancer/citus/gen/CitusTruncateGenerator.java b/src/sqlancer/citus/gen/CitusTruncateGenerator.java new file mode 100644 index 000000000..cf36ce9c2 --- /dev/null +++ b/src/sqlancer/citus/gen/CitusTruncateGenerator.java @@ -0,0 +1,20 @@ +package sqlancer.citus.gen; + +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.postgres.PostgresGlobalState; +import sqlancer.postgres.gen.PostgresTruncateGenerator; + +public final class CitusTruncateGenerator { + + private CitusTruncateGenerator() { + } + + public static SQLQueryAdapter create(PostgresGlobalState globalState) { + SQLQueryAdapter truncateQuery = PostgresTruncateGenerator.create(globalState); + ExpectedErrors errors = truncateQuery.getExpectedErrors(); + CitusCommon.addCitusErrors(errors); + return truncateQuery; + } + +} diff --git a/src/sqlancer/citus/gen/CitusVacuumGenerator.java b/src/sqlancer/citus/gen/CitusVacuumGenerator.java new file mode 100644 index 000000000..ae73dbf82 --- /dev/null +++ b/src/sqlancer/citus/gen/CitusVacuumGenerator.java @@ -0,0 +1,20 @@ +package sqlancer.citus.gen; + +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.postgres.PostgresGlobalState; +import sqlancer.postgres.gen.PostgresVacuumGenerator; + +public final class CitusVacuumGenerator { + + private CitusVacuumGenerator() { + } + + public static SQLQueryAdapter create(PostgresGlobalState globalState) { + SQLQueryAdapter vacuumQuery = PostgresVacuumGenerator.create(globalState); + ExpectedErrors errors = vacuumQuery.getExpectedErrors(); + CitusCommon.addCitusErrors(errors); + return vacuumQuery; + } + +} diff --git a/src/sqlancer/clickhouse/ClickHouseErrors.java b/src/sqlancer/clickhouse/ClickHouseErrors.java index 2c4d8d1b8..09fbe5ea8 100644 --- a/src/sqlancer/clickhouse/ClickHouseErrors.java +++ b/src/sqlancer/clickhouse/ClickHouseErrors.java @@ -41,6 +41,7 @@ public static List getExpectedExpressionErrors() { "Cannot convert out of range floating point value to integer type", "Unexpected inf or nan to integer conversion", "No such name in Block::erase", // https://github.com/ClickHouse/ClickHouse/issues/42769 "EMPTY_LIST_OF_COLUMNS_QUERIED", // https://github.com/ClickHouse/ClickHouse/issues/43003 + "EMPTY_LIST_OF_COLUMNS_PASSED", // https://github.com/ClickHouse/ClickHouse/pull/81835 "cannot get JOIN keys. (INVALID_JOIN_ON_EXPRESSION)", "AMBIGUOUS_IDENTIFIER", "CYCLIC_ALIASES", "Positional argument numeric constant expression is not representable as", "Positional argument must be constant with numeric type", " is out of bounds. Expected in range", diff --git a/src/sqlancer/clickhouse/ClickHouseSchema.java b/src/sqlancer/clickhouse/ClickHouseSchema.java index 97f4534a1..8f8f906ec 100644 --- a/src/sqlancer/clickhouse/ClickHouseSchema.java +++ b/src/sqlancer/clickhouse/ClickHouseSchema.java @@ -216,7 +216,7 @@ public static ClickHouseSchema fromConnection(SQLConnection con, String database for (String tableName : tableNames) { List databaseColumns = getTableColumns(con, tableName); List indexes = Collections.emptyList(); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); ClickHouseTable t = new ClickHouseTable(tableName, databaseColumns, indexes, isView); for (ClickHouseColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/clickhouse/gen/ClickHouseInsertGenerator.java b/src/sqlancer/clickhouse/gen/ClickHouseInsertGenerator.java index 9286185a7..3951f4b55 100644 --- a/src/sqlancer/clickhouse/gen/ClickHouseInsertGenerator.java +++ b/src/sqlancer/clickhouse/gen/ClickHouseInsertGenerator.java @@ -1,6 +1,5 @@ package sqlancer.clickhouse.gen; -import java.sql.SQLException; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -11,13 +10,11 @@ import sqlancer.clickhouse.ClickHouseSchema.ClickHouseTable; import sqlancer.clickhouse.ClickHouseToStringVisitor; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; public class ClickHouseInsertGenerator extends AbstractInsertGenerator { private final ClickHouseGlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); private final ClickHouseExpressionGenerator gen; public ClickHouseInsertGenerator(ClickHouseGlobalState globalState) { @@ -31,25 +28,19 @@ public ClickHouseInsertGenerator(ClickHouseGlobalState globalState) { ClickHouseErrors.addExpectedExpressionErrors(errors); } - public static SQLQueryAdapter getQuery(ClickHouseGlobalState globalState) throws SQLException { - return new ClickHouseInsertGenerator(globalState).get(); + public static SQLQueryAdapter getQuery(ClickHouseGlobalState globalState) { + return new ClickHouseInsertGenerator(globalState).getStatement(); } - private SQLQueryAdapter get() { + @Override + public void buildStatement() { ClickHouseTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = Collections.emptyList(); while (columns.isEmpty()) { columns = table.getRandomNonEmptyColumnSubset().stream().filter(c -> !c.isAlias() && !c.isMaterialized()) .collect(Collectors.toList()); } - sb.append("INSERT INTO "); - sb.append(table.getName()); - sb.append("("); - sb.append(columns.stream().map(c -> c.getName()).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); - return new SQLQueryAdapter(sb.toString(), errors); + buildInsertInto(table.getName(), columns); } @Override diff --git a/src/sqlancer/cnosdb/CnosDBBugs.java b/src/sqlancer/cnosdb/CnosDBBugs.java deleted file mode 100644 index 4e6eb96e9..000000000 --- a/src/sqlancer/cnosdb/CnosDBBugs.java +++ /dev/null @@ -1,13 +0,0 @@ -package sqlancer.cnosdb; - -public final class CnosDBBugs { - - // https://github.com/cnosdb/cnosdb/issues/786 - public static final boolean BUG786 = true; - - // https://github.com/apache/arrow-rs/issues/3547 - public static final boolean BUG3547 = true; - - private CnosDBBugs() { - } -} diff --git a/src/sqlancer/cnosdb/CnosDBComparatorHelper.java b/src/sqlancer/cnosdb/CnosDBComparatorHelper.java deleted file mode 100644 index 46b6ba615..000000000 --- a/src/sqlancer/cnosdb/CnosDBComparatorHelper.java +++ /dev/null @@ -1,145 +0,0 @@ -package sqlancer.cnosdb; - -import java.io.IOException; -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.IgnoreMeException; -import sqlancer.cnosdb.client.CnosDBResultSet; -import sqlancer.cnosdb.query.CnosDBSelectQuery; -import sqlancer.common.query.ExpectedErrors; - -public final class CnosDBComparatorHelper { - - private CnosDBComparatorHelper() { - } - - public static List getResultSetFirstColumnAsString(String queryString, ExpectedErrors errors, - CnosDBGlobalState state) throws Exception { - if (state.getOptions().logEachSelect()) { - // TODO: refactor me - state.getLogger().writeCurrent(queryString); - try { - state.getLogger().getCurrentFileWriter().flush(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - CnosDBSelectQuery q = new CnosDBSelectQuery(queryString, errors); - List result = new ArrayList<>(); - CnosDBResultSet resultSet; - try { - q.executeAndGet(state); - resultSet = q.getResultSet(); - if (resultSet == null) { - throw new AssertionError(q); - } - while (resultSet.next()) { - result.add(resultSet.getString(1)); - } - } catch (Exception e) { - if (e instanceof IgnoreMeException) { - throw e; - } - if (e instanceof NumberFormatException) { - throw new IgnoreMeException(); - } - if (e.getMessage() == null) { - throw new AssertionError(queryString, e); - } - if (errors.errorIsExpected(e.getMessage())) { - throw new IgnoreMeException(); - } - throw new AssertionError(queryString, e); - } - - return result; - } - - public static void assumeResultSetsAreEqual(List resultSet, List secondResultSet, - String originalQueryString, List combinedString, CnosDBGlobalState state) { - if (resultSet.size() != secondResultSet.size()) { - String queryFormatString = "-- %s;\n-- cardinality: %d"; - String firstQueryString = String.format(queryFormatString, originalQueryString, resultSet.size()); - String secondQueryString = String.format(queryFormatString, String.join(";", combinedString), - secondResultSet.size()); - state.getState().getLocalState().log(String.format("%s\n%s", firstQueryString, 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); - throw new AssertionError(assertionMessage); - } - - Set firstHashSet = new HashSet<>(resultSet); - Set secondHashSet = new HashSet<>(secondResultSet); - - if (!firstHashSet.equals(secondHashSet)) { - Set firstResultSetMisses = new HashSet<>(firstHashSet); - firstResultSetMisses.removeAll(secondHashSet); - Set secondResultSetMisses = new HashSet<>(secondHashSet); - secondResultSetMisses.removeAll(firstHashSet); - String queryFormatString = "-- %s;\n-- misses: %s"; - String firstQueryString = String.format(queryFormatString, originalQueryString, firstResultSetMisses); - 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\n%s", firstQueryString, secondQueryString)); - String assertionMessage = String.format("the content of the result sets mismatch!\n%s\n%s", - firstQueryString, secondQueryString); - throw new AssertionError(assertionMessage); - } - } - - public static void assumeResultSetsAreEqual(List resultSet, List secondResultSet, - String originalQueryString, List combinedString, CnosDBGlobalState 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, CnosDBGlobalState state, - ExpectedErrors errors) throws Exception { - List secondResultSet; - if (asUnion) { - String unionString = firstQueryString + " UNION ALL " + secondQueryString + " UNION ALL " - + thirdQueryString; - combinedString.add(unionString); - secondResultSet = getResultSetFirstColumnAsString(unionString, errors, state); - } else { - secondResultSet = new ArrayList<>(); - secondResultSet.addAll(getResultSetFirstColumnAsString(firstQueryString, errors, state)); - secondResultSet.addAll(getResultSetFirstColumnAsString(secondQueryString, errors, state)); - secondResultSet.addAll(getResultSetFirstColumnAsString(thirdQueryString, errors, state)); - combinedString.add(firstQueryString); - combinedString.add(secondQueryString); - combinedString.add(thirdQueryString); - } - return secondResultSet; - } - - public static List getCombinedResultSetNoDuplicates(String firstQueryString, String secondQueryString, - String thirdQueryString, List combinedString, boolean asUnion, CnosDBGlobalState state, - ExpectedErrors errors) throws Exception { - String unionString; - if (asUnion) { - unionString = firstQueryString + " UNION " + secondQueryString + " UNION " + thirdQueryString; - } else { - unionString = "SELECT DISTINCT * FROM (" + firstQueryString + " UNION ALL " + secondQueryString - + " UNION ALL " + thirdQueryString + ")"; - } - List secondResultSet; - combinedString.add(unionString); - secondResultSet = getResultSetFirstColumnAsString(unionString, errors, state); - return secondResultSet; - } -} diff --git a/src/sqlancer/cnosdb/CnosDBCompoundDataType.java b/src/sqlancer/cnosdb/CnosDBCompoundDataType.java deleted file mode 100644 index 034f0fc90..000000000 --- a/src/sqlancer/cnosdb/CnosDBCompoundDataType.java +++ /dev/null @@ -1,20 +0,0 @@ -package sqlancer.cnosdb; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public final class CnosDBCompoundDataType { - - private final CnosDBDataType dataType; - - private CnosDBCompoundDataType(CnosDBDataType dataType) { - this.dataType = dataType; - } - - public static CnosDBCompoundDataType create(CnosDBDataType type) { - return new CnosDBCompoundDataType(type); - } - - public CnosDBDataType getDataType() { - return dataType; - } -} diff --git a/src/sqlancer/cnosdb/CnosDBExpectedError.java b/src/sqlancer/cnosdb/CnosDBExpectedError.java deleted file mode 100644 index 61dba101b..000000000 --- a/src/sqlancer/cnosdb/CnosDBExpectedError.java +++ /dev/null @@ -1,87 +0,0 @@ -package sqlancer.cnosdb; - -import java.util.ArrayList; -import java.util.List; - -import sqlancer.common.query.ExpectedErrors; - -public final class CnosDBExpectedError { - - private CnosDBExpectedError() { - } - - public static List getExpectedErrors() { - ArrayList errors = new ArrayList<>(); - - errors.add("have the same name. Consider aliasing"); - errors.add( - "error: Optimizer rule 'projection_push_down' failed due to unexpected error: Schema error: Schema contains duplicate qualified field name"); - errors.add("Projection references non-aggregate values:"); - errors.add("External err: Schema error: No field named"); - errors.add( - "Optimizer rule 'common_sub_expression_eliminate' failed due to unexpected error: Schema error: No field named"); - errors.add("Binary"); - errors.add("Invalid pattern in LIKE expression"); - errors.add("If the projection contains the time column, it must contain the field column."); - errors.add("Schema error: No field named"); - errors.add("Optimizer rule 'simplify_expressions' failed due to unexpected error:"); - errors.add("err: Internal error: Optimizer rule 'projection_push_down' failed due to unexpected error"); - errors.add("Schema error: No field named "); - errors.add("err: External err: Schema error: No field named"); - errors.add("Optimizer rule 'simplify_expressions' failed due to unexpected error"); - errors.add("Csv error: CSV Writer does not support List"); - errors.add("This feature is not implemented: cross join."); - errors.add("Execution error: field position must be greater than zero"); - errors.add("First argument of `DATE_PART` must be non-null scalar Utf8"); - errors.add("Cannot create filter with non-boolean predicate 'NULL' returning Null"); - errors.add("requested character too large for encoding."); - errors.add("Can not find compatible types to compare Boolean with [Utf8]."); - errors.add("Cannot create filter with non-boolean predicate 'APPROXDISTINCT"); - errors.add("HAVING clause references non-aggregate values:"); - errors.add("Cannot create filter with non-boolean predicate"); - errors.add("negative substring length not allowed"); - errors.add("The function Sum does not support inputs of type Boolean."); - errors.add("The function Avg does not support inputs of type Boolean."); - errors.add("Percentile value must be between 0.0 and 1.0 inclusive"); - errors.add("Date part '' not supported"); - errors.add("Min/Max accumulator not implemented for type Boolean."); - errors.add("meta need get_series_id_by_filter"); - errors.add("Arrow: Cast error:"); - errors.add("Arrow error: Cast error:"); - errors.add("Datafusion: Execution error: Arrow error: External error: Arrow error: Cast error:"); - errors.add("Arrow error: Divide by zero error"); - errors.add("desired percentile argument must be float literal"); - errors.add("Unsupported CAST from Int32 to Timestamp(Nanosecond, None)"); - errors.add("Execution error: Date part"); - errors.add("Physical plan does not support logical expression MIN(Boolean"); - errors.add("The percentile argument for ApproxPercentileCont must be Float64, not Int64"); - errors.add("The percentile argument for ApproxPercentileContWithWeight must be Float64, not Int64."); - errors.add("Data type UInt64 not supported for binary operation '#' on dyn arrays."); - errors.add("Arrow: Divide by zero error"); - errors.add("The function ApproxPercentileCont does not support inputs of type Null."); - errors.add("can't be evaluated because there isn't a common type to coerce the types to"); - errors.add("This was likely caused by a bug in DataFusion's code and we would welcome that you file an bug"); - errors.add("The function ApproxMedian does not support inputs of type Null."); - errors.add("null character not permitted."); - errors.add("The percentile argument for ApproxPercentileCont must be Float64, not Null."); - errors.add("This feature is not implemented"); - errors.add("The function Avg does not support inputs of type Null."); - errors.add("Coercion from [Utf8, Timestamp(Nanosecond, Some(\\\"+00:00\\\"))]"); - errors.add( - "Coercion from [Utf8, Float64, Utf8] to the signature OneOf([Exact([Utf8, Int64]), Exact([LargeUtf8, Int64]), Exact([Utf8, Int64, Utf8]), Exact([LargeUtf8, Int64, Utf8]), Exact([Utf8, Int64, LargeUtf8]), Exact([LargeUtf8, Int64, LargeUtf8])]) failed."); - errors.add("Coercion from"); - - errors.add("Error parsing timestamp"); - errors.add("lpad requested length"); - errors.add("rpad requested length"); - errors.add("No function matches the given name and argument types"); - return errors; - } - - public static ExpectedErrors expectedErrors() { - ExpectedErrors res = new ExpectedErrors(); - res.addAll(getExpectedErrors()); - return res; - } - -} diff --git a/src/sqlancer/cnosdb/CnosDBGlobalState.java b/src/sqlancer/cnosdb/CnosDBGlobalState.java deleted file mode 100644 index 9f34e03a5..000000000 --- a/src/sqlancer/cnosdb/CnosDBGlobalState.java +++ /dev/null @@ -1,28 +0,0 @@ -package sqlancer.cnosdb; - -import sqlancer.ExecutionTimer; -import sqlancer.GlobalState; -import sqlancer.cnosdb.client.CnosDBConnection; -import sqlancer.common.query.Query; - -public class CnosDBGlobalState 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(); - } - } - - @Override - public CnosDBSchema readSchema() throws Exception { - return CnosDBSchema.fromConnection(getConnection()); - } -} diff --git a/src/sqlancer/cnosdb/CnosDBLoggableFactory.java b/src/sqlancer/cnosdb/CnosDBLoggableFactory.java deleted file mode 100644 index 407621c8b..000000000 --- a/src/sqlancer/cnosdb/CnosDBLoggableFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -package sqlancer.cnosdb; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import sqlancer.cnosdb.query.CnosDBOtherQuery; -import sqlancer.cnosdb.query.CnosDBQueryAdapter; -import sqlancer.common.log.Loggable; -import sqlancer.common.log.LoggableFactory; -import sqlancer.common.log.LoggedString; -import sqlancer.common.query.ExpectedErrors; -import sqlancer.common.query.Query; - -public class CnosDBLoggableFactory extends LoggableFactory { - - @Override - protected Loggable createLoggable(String input, String suffix) { - String completeString = input; - if (!input.endsWith(";")) { - completeString += ";"; - } - if (suffix != null && !suffix.isEmpty()) { - completeString += suffix; - } - return new LoggedString(completeString); - } - - @Override - public CnosDBQueryAdapter getQueryForStateToReproduce(String queryString) { - return new CnosDBOtherQuery(queryString, CnosDBExpectedError.expectedErrors()); - } - - @Override - public CnosDBQueryAdapter commentOutQuery(Query query) { - String queryString = query.getLogString(); - String newQueryString = "-- " + queryString; - ExpectedErrors errors = new ExpectedErrors(); - return new CnosDBOtherQuery(newQueryString, errors); - } - - @Override - protected Loggable infoToLoggable(String time, String databaseName, String databaseVersion, long seedValue) { - String sb = "-- Time: " + time + "\n" + "-- Database: " + databaseName + "\n" + "-- Database version: " - + databaseVersion + "\n" + "-- seed value: " + seedValue + "\n"; - return new LoggedString(sb); - } - - @Override - public Loggable convertStacktraceToLoggable(Throwable throwable) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - throwable.printStackTrace(pw); - return new LoggedString("--" + sw.toString().replace("\n", "\n--")); - } -} diff --git a/src/sqlancer/cnosdb/CnosDBOptions.java b/src/sqlancer/cnosdb/CnosDBOptions.java deleted file mode 100644 index f101c2d38..000000000 --- a/src/sqlancer/cnosdb/CnosDBOptions.java +++ /dev/null @@ -1,28 +0,0 @@ -package sqlancer.cnosdb; - -import java.util.List; - -import com.beust.jcommander.Parameter; -import com.beust.jcommander.Parameters; - -import sqlancer.DBMSSpecificOptions; - -@Parameters(separators = "=", commandDescription = "CnosDB (default port: " + CnosDBOptions.DEFAULT_PORT - + ", default host: " + CnosDBOptions.DEFAULT_HOST + ")") -public class CnosDBOptions implements DBMSSpecificOptions { - - public static final String DEFAULT_HOST = "localhost"; - public static final int DEFAULT_PORT = 31001; - - @Parameter(names = "--oracle", description = "Specifies which test oracle should be used for CnosDB") - public List oracle = List.of(CnosDBOracleFactory.QUERY_PARTITIONING); - - @Parameter(names = "--connection-url", description = "Specifies the URL for connecting to the CnosDB", arity = 1) - public String connectionURL = String.format("http://%s:%d", CnosDBOptions.DEFAULT_HOST, CnosDBOptions.DEFAULT_PORT); - - @Override - public List getTestOracleFactory() { - return oracle; - } - -} diff --git a/src/sqlancer/cnosdb/CnosDBOracleFactory.java b/src/sqlancer/cnosdb/CnosDBOracleFactory.java deleted file mode 100644 index 7cb9c4fc6..000000000 --- a/src/sqlancer/cnosdb/CnosDBOracleFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -package sqlancer.cnosdb; - -import java.util.ArrayList; -import java.util.List; - -import sqlancer.OracleFactory; -import sqlancer.cnosdb.oracle.CnosDBNoRECOracle; -import sqlancer.cnosdb.oracle.tlp.CnosDBTLPAggregateOracle; -import sqlancer.cnosdb.oracle.tlp.CnosDBTLPHavingOracle; -import sqlancer.cnosdb.oracle.tlp.CnosDBTLPWhereOracle; -import sqlancer.common.oracle.CompositeTestOracle; -import sqlancer.common.oracle.TestOracle; - -public enum CnosDBOracleFactory implements OracleFactory { - NOREC { - @Override - public TestOracle create(CnosDBGlobalState globalState) { - return new CnosDBNoRECOracle(globalState); - } - }, - HAVING { - @Override - public TestOracle create(CnosDBGlobalState globalState) { - return new CnosDBTLPHavingOracle(globalState); - } - - }, - QUERY_PARTITIONING { - @Override - public TestOracle create(CnosDBGlobalState globalState) { - List> oracles = new ArrayList<>(); - oracles.add(new CnosDBTLPWhereOracle(globalState)); - oracles.add(new CnosDBTLPHavingOracle(globalState)); - oracles.add(new CnosDBTLPAggregateOracle(globalState)); - return new CompositeTestOracle<>(oracles, globalState); - } - } - -} diff --git a/src/sqlancer/cnosdb/CnosDBProvider.java b/src/sqlancer/cnosdb/CnosDBProvider.java deleted file mode 100644 index 8b69c53b3..000000000 --- a/src/sqlancer/cnosdb/CnosDBProvider.java +++ /dev/null @@ -1,123 +0,0 @@ -package sqlancer.cnosdb; - -import java.util.Objects; - -import com.google.auto.service.AutoService; - -import sqlancer.AbstractAction; -import sqlancer.DatabaseProvider; -import sqlancer.IgnoreMeException; -import sqlancer.ProviderAdapter; -import sqlancer.Randomly; -import sqlancer.StatementExecutor; -import sqlancer.cnosdb.client.CnosDBClient; -import sqlancer.cnosdb.client.CnosDBConnection; -import sqlancer.cnosdb.gen.CnosDBInsertGenerator; -import sqlancer.cnosdb.gen.CnosDBTableGenerator; -import sqlancer.cnosdb.query.CnosDBOtherQuery; -import sqlancer.cnosdb.query.CnosDBQueryProvider; -import sqlancer.common.log.LoggableFactory; - -@AutoService(DatabaseProvider.class) -public class CnosDBProvider extends ProviderAdapter { - - protected String username; - protected String password; - protected String host; - protected int port; - protected String databaseName; - - public CnosDBProvider() { - super(CnosDBGlobalState.class, CnosDBOptions.class); - } - - protected CnosDBProvider(Class globalClass, Class optionClass) { - super(globalClass, optionClass); - } - - protected static int mapActions(CnosDBGlobalState globalState, Action a) { - Randomly r = globalState.getRandomly(); - int nrPerformed; - if (Objects.requireNonNull(a) == Action.INSERT) { - nrPerformed = r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); - } else { - throw new AssertionError(a); - } - return nrPerformed; - - } - - @Override - protected void checkViewsAreValid(CnosDBGlobalState globalState) { - } - - @Override - public void generateDatabase(CnosDBGlobalState globalState) throws Exception { - createTables(globalState, Randomly.fromOptions(4, 5, 6)); - prepareTables(globalState); - - } - - @Override - public CnosDBConnection createDatabase(CnosDBGlobalState globalState) throws Exception { - - username = globalState.getOptions().getUserName(); - password = globalState.getOptions().getPassword(); - host = globalState.getOptions().getHost(); - port = globalState.getOptions().getPort(); - databaseName = globalState.getDatabaseName(); - CnosDBClient client = new CnosDBClient(host, port, username, password, databaseName); - CnosDBConnection connection = new CnosDBConnection(client); - client.execute("DROP DATABASE IF EXISTS " + databaseName); - globalState.getState().logStatement("DROP DATABASE IF EXISTS " + databaseName); - client.execute("CREATE DATABASE " + databaseName); - globalState.getState().logStatement("CREATE DATABASE " + databaseName); - - return connection; - } - - protected void createTables(CnosDBGlobalState globalState, int numTables) throws Exception { - while (globalState.getSchema().getDatabaseTables().size() < numTables) { - String tableName = String.format("m%d", globalState.getSchema().getDatabaseTables().size()); - CnosDBOtherQuery createTable = CnosDBTableGenerator.generate(tableName); - globalState.executeStatement(createTable); - } - } - - protected void prepareTables(CnosDBGlobalState globalState) throws Exception { - StatementExecutor se = new StatementExecutor<>(globalState, Action.values(), - CnosDBProvider::mapActions, (q) -> { - if (globalState.getSchema().getDatabaseTables().isEmpty()) { - throw new IgnoreMeException(); - } - }); - se.executeStatements(); - } - - @Override - public String getDBMSName() { - return "CnosDB".toLowerCase(); - } - - @Override - public LoggableFactory getLoggableFactory() { - return new CnosDBLoggableFactory(); - } - - public enum Action implements AbstractAction { - INSERT(CnosDBInsertGenerator::insert); - - private final CnosDBQueryProvider sqlQueryProvider; - - Action(CnosDBQueryProvider sqlQueryProvider) { - this.sqlQueryProvider = sqlQueryProvider; - } - - @Override - public CnosDBOtherQuery getQuery(CnosDBGlobalState state) throws Exception { - return new CnosDBOtherQuery(sqlQueryProvider.getQuery(state).getQueryString(), - CnosDBExpectedError.expectedErrors()); - } - } - -} diff --git a/src/sqlancer/cnosdb/CnosDBSchema.java b/src/sqlancer/cnosdb/CnosDBSchema.java deleted file mode 100644 index 022969ce5..000000000 --- a/src/sqlancer/cnosdb/CnosDBSchema.java +++ /dev/null @@ -1,243 +0,0 @@ -package sqlancer.cnosdb; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import sqlancer.Randomly; -import sqlancer.cnosdb.ast.CnosDBConstant; -import sqlancer.cnosdb.client.CnosDBConnection; -import sqlancer.cnosdb.client.CnosDBResultSet; -import sqlancer.common.schema.AbstractRowValue; -import sqlancer.common.schema.AbstractSchema; -import sqlancer.common.schema.AbstractTable; -import sqlancer.common.schema.AbstractTableColumn; -import sqlancer.common.schema.AbstractTables; -import sqlancer.common.schema.TableIndex; - -public class CnosDBSchema extends AbstractSchema { - - private final String databaseName; - - public CnosDBSchema(List databaseTables, String databaseName) { - super(databaseTables); - this.databaseName = databaseName; - } - - public static CnosDBDataType getColumnType(String typeString) { - switch (typeString.toLowerCase()) { - case "bigint": - return CnosDBDataType.INT; - case "boolean": - return CnosDBDataType.BOOLEAN; - case "string": - return CnosDBDataType.STRING; - case "double": - return CnosDBDataType.DOUBLE; - case "bigint unsigned": - case "unsigned": - return CnosDBDataType.UINT; - case "timestamp(nanosecond)": - return CnosDBDataType.TIMESTAMP; - default: - throw new AssertionError(typeString); - } - } - - public static CnosDBSchema fromConnection(CnosDBConnection con) throws Exception { - CnosDBResultSet tablesRes = con.getClient().executeQuery("SHOW TABLES"); - - List tables = new ArrayList<>(); - while (tablesRes.next()) { - String tableName = tablesRes.getString(1); - List columns = getTableColumns(con, tableName); - tables.add(new CnosDBTable(tableName, columns)); - } - - return new CnosDBSchema(tables, con.getClient().getDatabase()); - } - - protected static List getTableColumns(CnosDBConnection con, String tableName) throws Exception { - CnosDBResultSet columnsRes = con.getClient().executeQuery("DESCRIBE TABLE " + tableName); - List columns = new ArrayList<>(); - CnosDBTable table = new CnosDBTable(tableName, columns); - while (columnsRes.next()) { - String columnName = columnsRes.getString(1); - String columnType = columnsRes.getString(3).toLowerCase(); - CnosDBDataType dataType = CnosDBSchema.getColumnType(columnsRes.getString(2)); - CnosDBColumn column; - if (columnType.contentEquals("time")) { - column = new CnosDBTimeColumn(); - } else if (columnType.contentEquals("tag")) { - column = new CnosDBTagColumn(columnName); - } else { - column = new CnosDBFieldColumn(columnName, dataType); - } - column.setTable(table); - columns.add(column); - } - - return columns; - } - - public CnosDBTables getRandomTableNonEmptyTables() { - return new CnosDBTables(Randomly.nonEmptySubset(getDatabaseTables())); - } - - public String getDatabaseName() { - return databaseName; - } - - public enum CnosDBDataType { - INT, BOOLEAN, STRING, DOUBLE, UINT, TIMESTAMP; - - public static CnosDBDataType getRandomType() { - return Randomly.fromOptions(values()); - } - - public static CnosDBDataType getRandomTypeWithoutTimeStamp() { - List dataTypes = new ArrayList<>(Arrays.asList(values())); - dataTypes.remove(TIMESTAMP); - return Randomly.fromList(dataTypes); - } - } - - public static class CnosDBColumn extends AbstractTableColumn { - - public CnosDBColumn(String name, CnosDBDataType columnType) { - super(name, null, columnType); - } - - public static CnosDBColumn createDummy(String name) { - return new CnosDBColumn(name, CnosDBDataType.INT); - } - - } - - public static class CnosDBTagColumn extends CnosDBColumn { - public CnosDBTagColumn(String name) { - super(name, CnosDBDataType.STRING); - } - } - - public static class CnosDBTimeColumn extends CnosDBColumn { - public CnosDBTimeColumn() { - super("TIME", CnosDBDataType.TIMESTAMP); - } - } - - public static class CnosDBFieldColumn extends CnosDBColumn { - public CnosDBFieldColumn(String name, CnosDBDataType columnType) { - super(name, columnType); - assert columnType != CnosDBDataType.TIMESTAMP; - } - } - - public static class CnosDBTables extends AbstractTables { - - public CnosDBTables(List tables) { - super(tables); - } - - public CnosDBRowValue getRandomRowValue(CnosDBConnection con) { - return null; - } - - public List getRandomColumnsWithOnlyOneField() { - ArrayList res = new ArrayList<>(); - this.getTables().forEach(table -> res.addAll(table.getRandomColumnsWithOnlyOneField())); - return res; - } - - } - - public static class CnosDBRowValue extends AbstractRowValue { - - protected CnosDBRowValue(CnosDBTables tables, Map values) { - super(tables, values); - } - - } - - public static class CnosDBTable extends AbstractTable { - - public CnosDBTable(String tableName, List columns) { - super(tableName, columns, null, false); - } - - @Override - public List getColumns() { - List res = super.getColumns(); - boolean hasTime = false; - for (CnosDBColumn column : res) { - if (column instanceof CnosDBTimeColumn) { - hasTime = true; - break; - } - } - assert hasTime; - - return res; - } - - public List getRandomColumnsWithOnlyOneField() { - ArrayList res = new ArrayList<>(); - boolean hasField = false; - for (CnosDBColumn column : getColumns()) { - if (column instanceof CnosDBTagColumn && Randomly.getBoolean()) { - res.add(column); - } else if (column instanceof CnosDBFieldColumn && !hasField) { - res.add(column); - hasField = true; - } - } - return res; - } - - // SELECT COUNT(*) FROM table; - @Override - public long getNrRows(CnosDBGlobalState globalState) { - long res; - try { - CnosDBResultSet tableCountRes = globalState.getConnection().getClient() - .executeQuery("SELECT COUNT(time) FROM " + this.name); - tableCountRes.next(); - res = tableCountRes.getLong(1); - } catch (Exception e) { - res = 0; - } - return res; - } - - @Override - public List getRandomNonEmptyColumnSubset() { - List selectedColumns = new ArrayList<>(); - ArrayList remainingColumns = new ArrayList<>(this.getColumns()); - - remainingColumns.removeIf(column -> column instanceof CnosDBTimeColumn); - CnosDBTimeColumn timeColumn = new CnosDBTimeColumn(); - timeColumn.setTable(this); - selectedColumns.add(timeColumn); - - remainingColumns.stream().filter(column -> column instanceof CnosDBTagColumn).findFirst().ifPresent(tag -> { - selectedColumns.add(tag); - remainingColumns.remove(tag); - }); - - remainingColumns.stream().filter(column -> column instanceof CnosDBFieldColumn).findFirst() - .ifPresent(field -> { - selectedColumns.add(field); - remainingColumns.remove(field); - }); - - int nr = Math.min(Randomly.smallNumber() + 1, remainingColumns.size()); - for (int i = 0; i < nr; i++) { - selectedColumns - .add(remainingColumns.remove((int) Randomly.getNotCachedInteger(0, remainingColumns.size()))); - } - return selectedColumns; - } - } - -} diff --git a/src/sqlancer/cnosdb/CnosDBToStringVisitor.java b/src/sqlancer/cnosdb/CnosDBToStringVisitor.java deleted file mode 100644 index 388e2ccd8..000000000 --- a/src/sqlancer/cnosdb/CnosDBToStringVisitor.java +++ /dev/null @@ -1,278 +0,0 @@ -package sqlancer.cnosdb; - -import sqlancer.Randomly; -import sqlancer.cnosdb.ast.CnosDBAggregate; -import sqlancer.cnosdb.ast.CnosDBBetweenOperation; -import sqlancer.cnosdb.ast.CnosDBBinaryLogicalOperation; -import sqlancer.cnosdb.ast.CnosDBCastOperation; -import sqlancer.cnosdb.ast.CnosDBColumnValue; -import sqlancer.cnosdb.ast.CnosDBConstant; -import sqlancer.cnosdb.ast.CnosDBExpression; -import sqlancer.cnosdb.ast.CnosDBFunction; -import sqlancer.cnosdb.ast.CnosDBInOperation; -import sqlancer.cnosdb.ast.CnosDBJoin; -import sqlancer.cnosdb.ast.CnosDBLikeOperation; -import sqlancer.cnosdb.ast.CnosDBOrderByTerm; -import sqlancer.cnosdb.ast.CnosDBPostfixOperation; -import sqlancer.cnosdb.ast.CnosDBPostfixText; -import sqlancer.cnosdb.ast.CnosDBPrefixOperation; -import sqlancer.cnosdb.ast.CnosDBSelect; -import sqlancer.cnosdb.ast.CnosDBSelect.CnosDBFromTable; -import sqlancer.cnosdb.ast.CnosDBSelect.CnosDBSubquery; -import sqlancer.cnosdb.ast.CnosDBSimilarTo; -import sqlancer.common.visitor.BinaryOperation; -import sqlancer.common.visitor.ToStringVisitor; - -public final class CnosDBToStringVisitor extends ToStringVisitor implements CnosDBVisitor { - - @Override - public void visitSpecific(CnosDBExpression expr) { - CnosDBVisitor.super.visit(expr); - } - - @Override - public void visit(CnosDBConstant constant) { - sb.append(constant.getTextRepresentation()); - } - - @Override - public String get() { - return sb.toString(); - } - - @Override - public void visit(CnosDBPostfixOperation op) { - sb.append("("); - visit(op.getExpression()); - sb.append(")"); - sb.append(" "); - sb.append(op.getOperatorTextRepresentation()); - } - - @Override - public void visit(CnosDBColumnValue c) { - sb.append(c.getColumn().getFullQualifiedName()); - } - - @Override - public void visit(CnosDBPrefixOperation op) { - sb.append(op.getTextRepresentation()); - sb.append(" ("); - visit(op.getExpression()); - sb.append(")"); - } - - @Override - public void visit(CnosDBFromTable from) { - sb.append(from.getTable().getName()); - } - - @Override - public void visit(CnosDBSubquery subquery) { - sb.append("("); - visit(subquery.getSelect()); - sb.append(") AS "); - sb.append(subquery.getName()); - } - - @Override - public void visit(CnosDBSelect s) { - sb.append("SELECT "); - switch (s.getSelectOption()) { - case DISTINCT: - sb.append("DISTINCT "); - if (s.getDistinctOnClause() != null) { - sb.append("ON ("); - visit(s.getDistinctOnClause()); - sb.append(") "); - } - break; - case ALL: - sb.append(Randomly.fromOptions("ALL ", "")); - break; - default: - throw new AssertionError(); - } - if (s.getFetchColumns() == null) { - sb.append("*"); - } else { - visit(s.getFetchColumns()); - } - sb.append(" FROM "); - visit(s.getFromList()); - - for (CnosDBJoin j : s.getJoinClauses()) { - sb.append(" "); - switch (j.getType()) { - case INNER: - if (Randomly.getBoolean()) { - sb.append("INNER "); - } - sb.append("JOIN"); - break; - case LEFT: - sb.append("LEFT OUTER JOIN"); - break; - case RIGHT: - sb.append("RIGHT OUTER JOIN"); - break; - case FULL: - sb.append("FULL OUTER JOIN"); - break; - // case CROSS: - // sb.append("CROSS JOIN"); - // break; - default: - throw new AssertionError(j.getType()); - } - sb.append(" "); - visit(j.getTableReference()); - // if (j.getType() != CnosDBJoinType.CROSS) { - sb.append(" ON "); - visit(j.getOnClause()); - // } - } - - if (s.getWhereClause() != null) { - sb.append(" WHERE "); - visit(s.getWhereClause()); - } - if (!s.getGroupByExpressions().isEmpty()) { - sb.append(" GROUP BY "); - visit(s.getGroupByExpressions()); - } - if (s.getHavingClause() != null) { - sb.append(" HAVING "); - visit(s.getHavingClause()); - - } - if (!s.getOrderByClauses().isEmpty()) { - sb.append(" ORDER BY "); - visit(s.getOrderByClauses()); - } - if (s.getLimitClause() != null) { - sb.append(" LIMIT "); - visit(s.getLimitClause()); - } - - if (s.getOffsetClause() != null) { - sb.append(" OFFSET "); - visit(s.getOffsetClause()); - } - } - - @Override - public void visit(CnosDBOrderByTerm op) { - visit(op.getExpr()); - sb.append(" "); - sb.append(op.getOrder()); - } - - @Override - public void visit(CnosDBFunction f) { - sb.append(f.getFunctionName()); - sb.append("("); - int i = 0; - for (CnosDBExpression arg : f.getArguments()) { - if (i++ != 0) { - sb.append(", "); - } - visit(arg); - } - sb.append(")"); - } - - @Override - public void visit(CnosDBCastOperation cast) { - sb.append("CAST( "); - visit(cast.getExpression()); - sb.append(" AS "); - appendType(cast); - sb.append(")"); - } - - private void appendType(CnosDBCastOperation cast) { - CnosDBCompoundDataType compoundType = cast.getCompoundType(); - switch (compoundType.getDataType()) { - case BOOLEAN: - sb.append("BOOLEAN"); - break; - case INT: - sb.append("BIGINT"); - break; - case STRING: - sb.append(Randomly.fromOptions("STRING")); - break; - case DOUBLE: - sb.append("DOUBLE"); - break; - case UINT: - sb.append("BIGINT UNSIGNED"); - break; - case TIMESTAMP: - sb.append("TIMESTAMP"); - break; - - default: - throw new AssertionError(cast.getType()); - } - } - - @Override - public void visit(CnosDBBetweenOperation op) { - sb.append("("); - visit(op.getExpr()); - sb.append(") BETWEEN ("); - visit(op.getLeft()); - sb.append(") AND ("); - visit(op.getRight()); - sb.append(")"); - } - - @Override - public void visit(CnosDBInOperation op) { - sb.append("("); - visit(op.getExpr()); - sb.append(")"); - if (!op.isTrue()) { - sb.append(" NOT"); - } - sb.append(" IN ("); - visit(op.getListElements()); - sb.append(")"); - } - - @Override - public void visit(CnosDBPostfixText op) { - visit(op.getExpr()); - sb.append(op.getText()); - } - - @Override - public void visit(CnosDBAggregate op) { - sb.append(op.getFunction()); - sb.append("("); - visit(op.getArgs()); - sb.append(")"); - } - - @Override - public void visit(CnosDBSimilarTo op) { - sb.append("("); - visit(op.getString()); - sb.append(" SIMILAR TO "); - visit(op.getSimilarTo()); - sb.append(")"); - } - - @Override - public void visit(CnosDBBinaryLogicalOperation op) { - super.visit((BinaryOperation) op); - } - - @Override - public void visit(CnosDBLikeOperation op) { - super.visit((BinaryOperation) op); - } - -} diff --git a/src/sqlancer/cnosdb/CnosDBVisitor.java b/src/sqlancer/cnosdb/CnosDBVisitor.java deleted file mode 100644 index 7c1af7224..000000000 --- a/src/sqlancer/cnosdb/CnosDBVisitor.java +++ /dev/null @@ -1,102 +0,0 @@ -package sqlancer.cnosdb; - -import sqlancer.cnosdb.ast.CnosDBAggregate; -import sqlancer.cnosdb.ast.CnosDBBetweenOperation; -import sqlancer.cnosdb.ast.CnosDBBinaryLogicalOperation; -import sqlancer.cnosdb.ast.CnosDBCastOperation; -import sqlancer.cnosdb.ast.CnosDBColumnValue; -import sqlancer.cnosdb.ast.CnosDBConstant; -import sqlancer.cnosdb.ast.CnosDBExpression; -import sqlancer.cnosdb.ast.CnosDBFunction; -import sqlancer.cnosdb.ast.CnosDBInOperation; -import sqlancer.cnosdb.ast.CnosDBLikeOperation; -import sqlancer.cnosdb.ast.CnosDBOrderByTerm; -import sqlancer.cnosdb.ast.CnosDBPostfixOperation; -import sqlancer.cnosdb.ast.CnosDBPostfixText; -import sqlancer.cnosdb.ast.CnosDBPrefixOperation; -import sqlancer.cnosdb.ast.CnosDBSelect; -import sqlancer.cnosdb.ast.CnosDBSelect.CnosDBFromTable; -import sqlancer.cnosdb.ast.CnosDBSelect.CnosDBSubquery; -import sqlancer.cnosdb.ast.CnosDBSimilarTo; - -public interface CnosDBVisitor { - - static String asString(CnosDBExpression expr) { - CnosDBToStringVisitor visitor = new CnosDBToStringVisitor(); - visitor.visit(expr); - return visitor.get(); - } - - void visit(CnosDBConstant constant); - - void visit(CnosDBPostfixOperation op); - - void visit(CnosDBColumnValue c); - - void visit(CnosDBPrefixOperation op); - - void visit(CnosDBSelect op); - - void visit(CnosDBOrderByTerm op); - - void visit(CnosDBFunction f); - - void visit(CnosDBCastOperation cast); - - void visit(CnosDBBetweenOperation op); - - void visit(CnosDBInOperation op); - - void visit(CnosDBPostfixText op); - - void visit(CnosDBAggregate op); - - void visit(CnosDBFromTable from); - - void visit(CnosDBSubquery subquery); - - void visit(CnosDBBinaryLogicalOperation op); - - void visit(CnosDBLikeOperation op); - - void visit(CnosDBSimilarTo op); - - default void visit(CnosDBExpression expression) { - if (expression instanceof CnosDBConstant) { - visit((CnosDBConstant) expression); - } else if (expression instanceof CnosDBPostfixOperation) { - visit((CnosDBPostfixOperation) expression); - } else if (expression instanceof CnosDBColumnValue) { - visit((CnosDBColumnValue) expression); - } else if (expression instanceof CnosDBPrefixOperation) { - visit((CnosDBPrefixOperation) expression); - } else if (expression instanceof CnosDBSelect) { - visit((CnosDBSelect) expression); - } else if (expression instanceof CnosDBOrderByTerm) { - visit((CnosDBOrderByTerm) expression); - } else if (expression instanceof CnosDBFunction) { - visit((CnosDBFunction) expression); - } else if (expression instanceof CnosDBCastOperation) { - visit((CnosDBCastOperation) expression); - } else if (expression instanceof CnosDBBetweenOperation) { - visit((CnosDBBetweenOperation) expression); - } else if (expression instanceof CnosDBInOperation) { - visit((CnosDBInOperation) expression); - } else if (expression instanceof CnosDBAggregate) { - visit((CnosDBAggregate) expression); - } else if (expression instanceof CnosDBPostfixText) { - visit((CnosDBPostfixText) expression); - } else if (expression instanceof CnosDBSimilarTo) { - visit((CnosDBSimilarTo) expression); - } else if (expression instanceof CnosDBFromTable) { - visit((CnosDBFromTable) expression); - } else if (expression instanceof CnosDBSubquery) { - visit((CnosDBSubquery) expression); - } else if (expression instanceof CnosDBLikeOperation) { - visit((CnosDBLikeOperation) expression); - } else { - throw new AssertionError(expression); - } - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBAggregate.java b/src/sqlancer/cnosdb/ast/CnosDBAggregate.java deleted file mode 100644 index df30717b4..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBAggregate.java +++ /dev/null @@ -1,113 +0,0 @@ -package sqlancer.cnosdb.ast; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBBugs; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.ast.CnosDBAggregate.CnosDBAggregateFunction; -import sqlancer.common.ast.FunctionNode; - -public class CnosDBAggregate extends FunctionNode - implements CnosDBExpression { - - public CnosDBAggregate(List args, CnosDBAggregateFunction func) { - super(func, args); - } - - public enum CnosDBAggregateFunction { - AVG(CnosDBDataType.DOUBLE), - MAX(CnosDBDataType.DOUBLE, CnosDBDataType.INT, CnosDBDataType.STRING, CnosDBDataType.TIMESTAMP, - CnosDBDataType.UINT), - MIN(CnosDBDataType.DOUBLE, CnosDBDataType.INT, CnosDBDataType.STRING, CnosDBDataType.TIMESTAMP, - CnosDBDataType.UINT), - COUNT(CnosDBDataType.INT) { - @Override - public CnosDBDataType[] getArgsTypes(CnosDBDataType returnType) { - return new CnosDBDataType[] { CnosDBDataType.getRandomType() }; - } - }, - SUM(CnosDBDataType.INT, CnosDBDataType.DOUBLE, CnosDBDataType.UINT), APPROX_MEDIAN(CnosDBDataType.DOUBLE), - - VAR(CnosDBDataType.DOUBLE), VAR_SAMP(CnosDBDataType.DOUBLE), VAR_POP(CnosDBDataType.DOUBLE), - STDDEV(CnosDBDataType.DOUBLE), STDDEV_SAMP(CnosDBDataType.DOUBLE), STDDEV_POP(CnosDBDataType.DOUBLE), - COVAR(CnosDBDataType.DOUBLE) { - @Override - public CnosDBDataType[] getArgsTypes(CnosDBDataType returnType) { - return new CnosDBDataType[] { CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE }; - } - }, - COVAR_SAMP(CnosDBDataType.DOUBLE) { - @Override - public CnosDBDataType[] getArgsTypes(CnosDBDataType returnType) { - return new CnosDBDataType[] { CnosDBDataType.DOUBLE, CnosDBDataType.INT }; - } - }, - CORR(CnosDBDataType.DOUBLE) { - @Override - public CnosDBDataType[] getArgsTypes(CnosDBDataType returnType) { - return new CnosDBDataType[] { CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE }; - } - }, - COVAR_POP(CnosDBDataType.DOUBLE) { - @Override - public CnosDBDataType[] getArgsTypes(CnosDBDataType returnType) { - return new CnosDBDataType[] { CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE }; - } - }, - - APPROX_PERCENTILE_CONT(CnosDBDataType.DOUBLE) { - @Override - public CnosDBDataType[] getArgsTypes(CnosDBDataType returnType) { - return new CnosDBDataType[] { CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE }; - } - }, - APPROX_PERCENTILE_CONT_WITH_WEIGHT(CnosDBDataType.DOUBLE) { - @Override - public CnosDBDataType[] getArgsTypes(CnosDBDataType returnType) { - return new CnosDBDataType[] { CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE }; - } - }, - APPROX_DISTINCT(CnosDBDataType.UINT), GROUPING(CnosDBDataType.INT), ARRAY_AGG(CnosDBDataType.STRING); - - private final CnosDBDataType[] supportedReturnTypes; - - CnosDBAggregateFunction(CnosDBDataType... supportedReturnTypes) { - this.supportedReturnTypes = supportedReturnTypes.clone(); - } - - public static List getAggregates(CnosDBDataType type) { - List res = Stream.of(values()).filter(p -> p.supportsReturnType(type)) - .collect(Collectors.toList()); - if (CnosDBBugs.BUG786) { - res.removeAll(List.of(VAR, VAR_POP, VAR_SAMP, STDDEV, STDDEV_POP, STDDEV_SAMP, CORR, COVAR, COVAR_POP, - COVAR_SAMP, APPROX_PERCENTILE_CONT_WITH_WEIGHT, APPROX_DISTINCT, APPROX_PERCENTILE_CONT, - APPROX_PERCENTILE_CONT_WITH_WEIGHT, GROUPING, ARRAY_AGG)); - } - - return res; - } - - public CnosDBDataType[] getArgsTypes(CnosDBDataType returnType) { - return new CnosDBDataType[] { returnType }; - } - - public boolean supportsReturnType(CnosDBDataType returnType) { - return Arrays.stream(supportedReturnTypes).anyMatch(t -> t == returnType) - || supportedReturnTypes.length == 0; - } - - public CnosDBDataType getRandomReturnType() { - if (supportedReturnTypes.length == 0) { - return Randomly.fromOptions(CnosDBDataType.getRandomType()); - } else { - return Randomly.fromOptions(supportedReturnTypes); - } - } - - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBAlias.java b/src/sqlancer/cnosdb/ast/CnosDBAlias.java deleted file mode 100644 index 86bba199f..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBAlias.java +++ /dev/null @@ -1,35 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.common.visitor.UnaryOperation; - -public class CnosDBAlias implements UnaryOperation, CnosDBExpression { - - private final CnosDBExpression expr; - private final String alias; - - public CnosDBAlias(CnosDBExpression expr, String alias) { - this.expr = expr; - this.alias = alias; - } - - @Override - public CnosDBExpression getExpression() { - return expr; - } - - @Override - public String getOperatorRepresentation() { - return " as " + alias; - } - - @Override - public OperatorKind getOperatorKind() { - return OperatorKind.POSTFIX; - } - - @Override - public boolean omitBracketsWhenPrinting() { - return true; - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBBetweenOperation.java b/src/sqlancer/cnosdb/ast/CnosDBBetweenOperation.java deleted file mode 100644 index d0addced1..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBBetweenOperation.java +++ /dev/null @@ -1,34 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public final class CnosDBBetweenOperation implements CnosDBExpression { - - private final CnosDBExpression expr; - private final CnosDBExpression left; - private final CnosDBExpression right; - - public CnosDBBetweenOperation(CnosDBExpression expr, CnosDBExpression left, CnosDBExpression right) { - this.expr = expr; - this.left = left; - this.right = right; - } - - public CnosDBExpression getExpr() { - return expr; - } - - public CnosDBExpression getLeft() { - return left; - } - - public CnosDBExpression getRight() { - return right; - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBBinaryArithmeticOperation.java b/src/sqlancer/cnosdb/ast/CnosDBBinaryArithmeticOperation.java deleted file mode 100644 index acf3e93d5..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBBinaryArithmeticOperation.java +++ /dev/null @@ -1,69 +0,0 @@ -package sqlancer.cnosdb.ast; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.ast.CnosDBBinaryArithmeticOperation.CnosDBBinaryOperator; -import sqlancer.common.ast.BinaryOperatorNode; - -public class CnosDBBinaryArithmeticOperation extends BinaryOperatorNode - implements CnosDBExpression { - - public CnosDBBinaryArithmeticOperation(CnosDBExpression left, CnosDBExpression right, CnosDBBinaryOperator op) { - super(left, right, op); - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.INT; - } - - public enum CnosDBBinaryOperator implements BinaryOperatorNode.Operator { - - ADDITION("+") { - }, - SUBTRACTION("-") { - }, - MULTIPLICATION("*") { - }, - DIVISION("/") { - - }, - MODULO("%") { - }, - EXPONENTIATION("^") { - }; - - private final String textRepresentation; - - CnosDBBinaryOperator(String textRepresentation) { - this.textRepresentation = textRepresentation; - } - - public static CnosDBBinaryOperator getRandom(CnosDBDataType dataType) { - List ops = new ArrayList<>(Arrays.asList(values())); - switch (dataType) { - case DOUBLE: - case UINT: - case STRING: - ops.remove(EXPONENTIATION); - ops.remove(MODULO); - break; - default: - break; - } - - return Randomly.fromList(ops); - } - - @Override - public String getTextRepresentation() { - return textRepresentation; - } - - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBBinaryComparisonOperation.java b/src/sqlancer/cnosdb/ast/CnosDBBinaryComparisonOperation.java deleted file mode 100644 index af38849c9..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBBinaryComparisonOperation.java +++ /dev/null @@ -1,57 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.ast.CnosDBBinaryComparisonOperation.CnosDBBinaryComparisonOperator; -import sqlancer.common.ast.BinaryOperatorNode; - -public class CnosDBBinaryComparisonOperation - extends BinaryOperatorNode implements CnosDBExpression { - - public CnosDBBinaryComparisonOperation(CnosDBExpression left, CnosDBExpression right, - CnosDBBinaryComparisonOperator op) { - super(left, right, op); - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } - - public enum CnosDBBinaryComparisonOperator implements BinaryOperatorNode.Operator { - EQUALS("=") { - }, - IS_DISTINCT("IS DISTINCT FROM") { - }, - IS_NOT_DISTINCT("IS NOT DISTINCT FROM") { - }, - NOT_EQUALS("!=") { - }, - LESS("<") { - }, - LESS_EQUALS("<=") { - }, - GREATER(">") { - }, - GREATER_EQUALS(">=") { - - }; - - private final String textRepresentation; - - CnosDBBinaryComparisonOperator(String textRepresentation) { - this.textRepresentation = textRepresentation; - } - - public static CnosDBBinaryComparisonOperator getRandom() { - return Randomly.fromOptions(CnosDBBinaryComparisonOperator.values()); - } - - @Override - public String getTextRepresentation() { - return textRepresentation; - } - - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBBinaryLogicalOperation.java b/src/sqlancer/cnosdb/ast/CnosDBBinaryLogicalOperation.java deleted file mode 100644 index bad8a3b75..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBBinaryLogicalOperation.java +++ /dev/null @@ -1,33 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.ast.CnosDBBinaryLogicalOperation.BinaryLogicalOperator; -import sqlancer.common.ast.BinaryOperatorNode; - -public class CnosDBBinaryLogicalOperation extends BinaryOperatorNode - implements CnosDBExpression { - - public CnosDBBinaryLogicalOperation(CnosDBExpression left, CnosDBExpression right, BinaryLogicalOperator op) { - super(left, right, op); - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } - - public enum BinaryLogicalOperator implements BinaryOperatorNode.Operator { - AND, OR; - - public static BinaryLogicalOperator getRandom() { - return Randomly.fromOptions(values()); - } - - @Override - public String getTextRepresentation() { - return toString(); - } - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBCastOperation.java b/src/sqlancer/cnosdb/ast/CnosDBCastOperation.java deleted file mode 100644 index 41db62d81..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBCastOperation.java +++ /dev/null @@ -1,60 +0,0 @@ -package sqlancer.cnosdb.ast; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import sqlancer.cnosdb.CnosDBCompoundDataType; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public class CnosDBCastOperation implements CnosDBExpression { - - private final CnosDBExpression expression; - private final CnosDBCompoundDataType type; - - public CnosDBCastOperation(CnosDBExpression expression, CnosDBCompoundDataType type) { - if (expression == null) { - throw new AssertionError(); - } - this.expression = expression; - this.type = type; - } - - public static List canCastTo(CnosDBDataType dataType) { - List options = new ArrayList<>(Arrays.asList(CnosDBDataType.values())); - - switch (dataType) { - case UINT: - case BOOLEAN: - case DOUBLE: - options.remove(CnosDBDataType.TIMESTAMP); - break; - case TIMESTAMP: - options.remove(CnosDBDataType.BOOLEAN); - options.remove(CnosDBDataType.UINT); - options.remove(CnosDBDataType.DOUBLE); - break; - default: - break; - } - return options; - } - - @Override - public CnosDBDataType getExpressionType() { - return type.getDataType(); - } - - public CnosDBExpression getExpression() { - return expression; - } - - public CnosDBDataType getType() { - return type.getDataType(); - } - - public CnosDBCompoundDataType getCompoundType() { - return type; - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBColumnValue.java b/src/sqlancer/cnosdb/ast/CnosDBColumnValue.java deleted file mode 100644 index f90b6120f..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBColumnValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBColumn; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public class CnosDBColumnValue implements CnosDBExpression { - - private final CnosDBColumn c; - - public CnosDBColumnValue(CnosDBColumn c) { - this.c = c; - } - - public static CnosDBColumnValue create(CnosDBColumn c) { - return new CnosDBColumnValue(c); - } - - @Override - public CnosDBDataType getExpressionType() { - return c.getType(); - } - - public CnosDBColumn getColumn() { - return c; - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBConcatOperation.java b/src/sqlancer/cnosdb/ast/CnosDBConcatOperation.java deleted file mode 100644 index 6821f83b8..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBConcatOperation.java +++ /dev/null @@ -1,22 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.common.ast.BinaryNode; - -public class CnosDBConcatOperation extends BinaryNode implements CnosDBExpression { - - public CnosDBConcatOperation(CnosDBExpression left, CnosDBExpression right) { - super(left, right); - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.STRING; - } - - @Override - public String getOperatorRepresentation() { - return "||"; - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBConstant.java b/src/sqlancer/cnosdb/ast/CnosDBConstant.java deleted file mode 100644 index 42ecd3908..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBConstant.java +++ /dev/null @@ -1,520 +0,0 @@ -package sqlancer.cnosdb.ast; - -import java.math.BigDecimal; -import java.text.SimpleDateFormat; -import java.util.Date; - -import sqlancer.IgnoreMeException; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public abstract class CnosDBConstant implements CnosDBExpression { - - public static CnosDBConstant createNullConstant() { - return new CnosDBNullConstant(); - } - - public static CnosDBConstant createIntConstant(long val) { - return new IntConstant(val, false); - } - - public static CnosDBConstant createBooleanConstant(boolean val) { - return new BooleanConstant(val); - } - - public static CnosDBConstant createFalse() { - return createBooleanConstant(false); - } - - public static CnosDBConstant createTrue() { - return createBooleanConstant(true); - } - - public static CnosDBConstant createStringConstant(String string) { - return new StringConstant(string); - } - - public static CnosDBConstant createDoubleConstant(double val) { - return new DoubleConstant(val); - } - - public static CnosDBConstant createUintConstant(long val) { - return new IntConstant(val, true); - } - - public static CnosDBConstant createTimeStampConstant(long val) { - return new TimeStampConstant(val); - } - - public abstract String getTextRepresentation(); - - public String asString() { - throw new UnsupportedOperationException(this.toString()); - } - - public boolean isString() { - return false; - } - - public boolean isNull() { - return false; - } - - public boolean asBoolean() { - throw new UnsupportedOperationException(this.toString()); - } - - public long asInt() { - throw new UnsupportedOperationException(this.toString()); - } - - public double asDouble() { - throw new UnsupportedOperationException(this.toString()); - } - - public boolean isBoolean() { - return false; - } - - public abstract CnosDBConstant isEquals(CnosDBConstant rightVal); - - public boolean isInt() { - return false; - } - - protected abstract CnosDBConstant isLessThan(CnosDBConstant rightVal); - - @Override - public String toString() { - return getTextRepresentation(); - } - - public abstract CnosDBConstant cast(CnosDBDataType type); - - public static class BooleanConstant extends CnosDBConstant { - - private final boolean value; - - public BooleanConstant(boolean value) { - this.value = value; - } - - @Override - public String getTextRepresentation() { - return value ? "TRUE" : "FALSE"; - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } - - @Override - public boolean asBoolean() { - return value; - } - - @Override - public boolean isBoolean() { - return true; - } - - @Override - public CnosDBConstant isEquals(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.isBoolean()) { - return CnosDBConstant.createBooleanConstant(value == rightVal.asBoolean()); - } else if (rightVal.isString()) { - return CnosDBConstant.createBooleanConstant(value == rightVal.cast(CnosDBDataType.BOOLEAN).asBoolean()); - } else { - throw new AssertionError(rightVal); - } - } - - @Override - protected CnosDBConstant isLessThan(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.isString()) { - return isLessThan(rightVal.cast(CnosDBDataType.BOOLEAN)); - } else { - assert rightVal.isBoolean(); - return CnosDBConstant.createBooleanConstant((value ? 1 : 0) < (rightVal.asBoolean() ? 1 : 0)); - } - } - - @Override - public CnosDBConstant cast(CnosDBDataType type) { - switch (type) { - case BOOLEAN: - return this; - case INT: - return CnosDBConstant.createIntConstant(value ? 1 : 0); - case UINT: - return CnosDBConstant.createUintConstant(value ? 1 : 0); - case STRING: - return CnosDBConstant.createStringConstant(value ? "true" : "false"); - default: - return null; - } - } - - } - - public static class CnosDBNullConstant extends CnosDBConstant { - - @Override - public String getTextRepresentation() { - return "NULL"; - } - - @Override - public CnosDBDataType getExpressionType() { - return null; - } - - @Override - public boolean isNull() { - return true; - } - - @Override - public CnosDBConstant isEquals(CnosDBConstant rightVal) { - return CnosDBConstant.createNullConstant(); - } - - @Override - protected CnosDBConstant isLessThan(CnosDBConstant rightVal) { - return CnosDBConstant.createNullConstant(); - } - - @Override - public CnosDBConstant cast(CnosDBDataType type) { - return CnosDBConstant.createNullConstant(); - } - } - - public static class StringConstant extends CnosDBConstant { - - private final String value; - - public StringConstant(String value) { - this.value = value; - } - - @Override - public String getTextRepresentation() { - return String.format("'%s'", value.replace("'", "''")); - } - - @Override - public CnosDBConstant isEquals(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.isInt()) { - return cast(CnosDBDataType.INT).isEquals(rightVal.cast(CnosDBDataType.INT)); - } else if (rightVal.isBoolean()) { - return cast(CnosDBDataType.BOOLEAN).isEquals(rightVal.cast(CnosDBDataType.BOOLEAN)); - } else if (rightVal.isString()) { - return CnosDBConstant.createBooleanConstant(value.contentEquals(rightVal.asString())); - } else { - throw new AssertionError(rightVal); - } - } - - @Override - protected CnosDBConstant isLessThan(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.isInt()) { - return cast(CnosDBDataType.INT).isLessThan(rightVal.cast(CnosDBDataType.INT)); - } else if (rightVal.isBoolean()) { - return cast(CnosDBDataType.BOOLEAN).isLessThan(rightVal.cast(CnosDBDataType.BOOLEAN)); - } else if (rightVal.isString()) { - return CnosDBConstant.createBooleanConstant(value.compareTo(rightVal.asString()) < 0); - } else { - throw new AssertionError(rightVal); - } - } - - @Override - public CnosDBConstant cast(CnosDBDataType type) { - if (type == CnosDBDataType.STRING) { - return this; - } - String s = value.trim(); - switch (type) { - case BOOLEAN: - try { - return CnosDBConstant.createBooleanConstant(Long.parseLong(s) != 0); - } catch (NumberFormatException ignored) { - } - switch (s.toUpperCase()) { - case "T": - case "TR": - case "TRU": - case "TRUE": - case "1": - case "YES": - case "YE": - case "Y": - case "ON": - return CnosDBConstant.createTrue(); - case "F": - case "FA": - case "FAL": - case "FALS": - case "FALSE": - case "N": - case "NO": - case "OF": - case "OFF": - default: - return CnosDBConstant.createFalse(); - } - case INT: - try { - return CnosDBConstant.createIntConstant(Long.parseLong(s)); - } catch (NumberFormatException e) { - return CnosDBConstant.createIntConstant(-1); - } - case UINT: - try { - return CnosDBConstant.createUintConstant(Long.parseUnsignedLong(s)); - } catch (NumberFormatException e) { - return CnosDBConstant.createUintConstant(0); - } - case DOUBLE: - try { - return CnosDBConstant.createDoubleConstant(Double.parseDouble(s)); - } catch (NumberFormatException e) { - return CnosDBConstant.createDoubleConstant(0.0); - } - - default: - return null; - } - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.STRING; - } - - @Override - public boolean isString() { - return true; - } - - @Override - public String asString() { - return value; - } - - } - - public static class IntConstant extends CnosDBConstant { - - private final long val; - private final boolean unsigned; - - public IntConstant(long val, boolean unsigned) { - this.val = val; - this.unsigned = unsigned; - } - - @Override - public String getTextRepresentation() { - if (unsigned) { - return Long.toUnsignedString(val); - } else { - return String.valueOf(val); - } - } - - @Override - public CnosDBDataType getExpressionType() { - if (unsigned) { - return CnosDBDataType.UINT; - } - return CnosDBDataType.INT; - } - - @Override - public long asInt() { - return val; - } - - @Override - public double asDouble() { - return val; - } - - @Override - public boolean isInt() { - return true; - } - - @Override - public CnosDBConstant isEquals(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.isBoolean()) { - return cast(CnosDBDataType.BOOLEAN).isEquals(rightVal); - } else if (rightVal.isInt()) { - return CnosDBConstant.createBooleanConstant(val == rightVal.asInt()); - } else if (rightVal.isString()) { - return CnosDBConstant.createBooleanConstant(val == rightVal.cast(CnosDBDataType.INT).asInt()); - } else { - throw new AssertionError(rightVal); - } - } - - @Override - protected CnosDBConstant isLessThan(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.isInt()) { - return CnosDBConstant.createBooleanConstant(val < rightVal.asInt()); - } else if (rightVal.isBoolean()) { - throw new AssertionError(rightVal); - } else if (rightVal.getExpressionType() == CnosDBDataType.UINT) { - return CnosDBConstant.createBooleanConstant(Long.compareUnsigned(val, rightVal.asInt()) < 0); - } else if (rightVal.isString()) { - return CnosDBConstant.createBooleanConstant(val < rightVal.cast(CnosDBDataType.INT).asInt()); - } else { - throw new IgnoreMeException(); - } - - } - - @Override - public CnosDBConstant cast(CnosDBDataType type) { - switch (type) { - case BOOLEAN: - return CnosDBConstant.createBooleanConstant(val != 0); - case INT: - return CnosDBConstant.createIntConstant(val); - case STRING: - return CnosDBConstant.createStringConstant(String.valueOf(val)); - case UINT: - return CnosDBConstant.createUintConstant(val); - case DOUBLE: - return CnosDBConstant.createDoubleConstant(val); - default: - return null; - } - } - } - - public static class TimeStampConstant extends CnosDBConstant { - final long val; - - TimeStampConstant(long time) { - val = time; - } - - @Override - public String getTextRepresentation() { - return "CAST (" + val + " AS TIMESTAMP)"; - } - - @Override - public CnosDBConstant isEquals(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return createNullConstant(); - } else if (rightVal.getExpressionType() == CnosDBDataType.TIMESTAMP) { - return createBooleanConstant(val == rightVal.asInt()); - } else { - throw new AssertionError(rightVal); - } - } - - @Override - protected CnosDBConstant isLessThan(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.getExpressionType() == CnosDBDataType.TIMESTAMP) { - return CnosDBConstant.createBooleanConstant(val < rightVal.asInt()); - } else { - throw new AssertionError(rightVal); - } - } - - @Override - public CnosDBConstant cast(CnosDBDataType type) { - switch (type) { - case INT: - return createIntConstant(val); - case STRING: - final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); - return CnosDBConstant.createStringConstant(dateFormat.format(new Date(val))); - default: - return null; - } - } - - @Override - public long asInt() { - return val; - } - - } - - public static class DoubleConstant extends CnosDBConstant { - - private final double val; - - public DoubleConstant(double val) { - this.val = val; - } - - @Override - public String getTextRepresentation() { - if (Double.isFinite(val)) { - BigDecimal bigDecimal = new BigDecimal(val); - return bigDecimal.toPlainString(); - } else { - return String.valueOf(0.0); - } - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.DOUBLE; - } - - @Override - public boolean isNull() { - return false; - } - - @Override - protected CnosDBConstant isLessThan(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.isBoolean()) { - return cast(CnosDBDataType.BOOLEAN).isLessThan(rightVal); - } else { - return CnosDBConstant.createBooleanConstant(val < rightVal.cast(CnosDBDataType.DOUBLE).asDouble()); - } - } - - @Override - public CnosDBConstant isEquals(CnosDBConstant rightVal) { - if (rightVal.isNull()) { - return CnosDBConstant.createNullConstant(); - } else if (rightVal.isBoolean()) { - return cast(CnosDBDataType.BOOLEAN).isEquals(rightVal); - } else { - return CnosDBConstant.createBooleanConstant(val == rightVal.cast(CnosDBDataType.DOUBLE).asDouble()); - } - } - - @Override - public CnosDBConstant cast(CnosDBDataType type) { - return null; - } - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBExpression.java b/src/sqlancer/cnosdb/ast/CnosDBExpression.java deleted file mode 100644 index 63997a0f5..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBExpression.java +++ /dev/null @@ -1,14 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public interface CnosDBExpression { - - default CnosDBDataType getExpressionType() { - return null; - } - - default CnosDBConstant getExpectedValue() { - throw new AssertionError("Not impl"); - } -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBFunction.java b/src/sqlancer/cnosdb/ast/CnosDBFunction.java deleted file mode 100644 index 7a35d703e..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBFunction.java +++ /dev/null @@ -1,30 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public class CnosDBFunction implements CnosDBExpression { - - private final String func; - private final CnosDBExpression[] args; - private final CnosDBDataType returnType; - - public CnosDBFunction(CnosDBFunctionWithUnknownResult f, CnosDBDataType returnType, CnosDBExpression... args) { - this.func = f.getName(); - this.returnType = returnType; - this.args = args.clone(); - } - - public String getFunctionName() { - return func; - } - - public CnosDBExpression[] getArguments() { - return args.clone(); - } - - @Override - public CnosDBDataType getExpressionType() { - return returnType; - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBFunctionWithUnknownResult.java b/src/sqlancer/cnosdb/ast/CnosDBFunctionWithUnknownResult.java deleted file mode 100644 index 485f2309d..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBFunctionWithUnknownResult.java +++ /dev/null @@ -1,104 +0,0 @@ -package sqlancer.cnosdb.ast; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import sqlancer.cnosdb.CnosDBBugs; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.gen.CnosDBExpressionGenerator; - -public enum CnosDBFunctionWithUnknownResult { - - // String functions - ASCII("ascii", CnosDBDataType.INT, CnosDBDataType.STRING), - BTRIM("btrim", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING), - CHAR_LENGTH("char_length", CnosDBDataType.INT, CnosDBDataType.STRING), - CHARACTER_LENGTH("character_length", CnosDBDataType.INT, CnosDBDataType.STRING), - CONCAT("concat", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING), - CONCAT_WS("concat_ws", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING), - CHR("chr", CnosDBDataType.STRING, CnosDBDataType.INT), - BIT_LENGTH("bit_length", CnosDBDataType.INT, CnosDBDataType.STRING), - INITCAP("initcap", CnosDBDataType.STRING, CnosDBDataType.STRING), - - LEFT("left", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT), - LENGTH("length", CnosDBDataType.UINT, CnosDBDataType.STRING), - LOWER("lower", CnosDBDataType.STRING, CnosDBDataType.STRING), - UPPER("upper", CnosDBDataType.STRING, CnosDBDataType.STRING), - LPAD3("lpad", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT, CnosDBDataType.STRING), - LPAD2("lpad", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT), - RPAD3("rpad", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT, CnosDBDataType.STRING), - RPAD2("rpad", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT), - LTRIM("ltrim", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING), - OCTET_LENGTH("octet_length", CnosDBDataType.INT, CnosDBDataType.STRING), - // REPEAT("repeat", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT), - REPLACE("replace", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING), - REVERSE("reverse", CnosDBDataType.STRING, CnosDBDataType.STRING), - RIGHT("right", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT), - RTRIM("rtrim", CnosDBDataType.STRING, CnosDBDataType.STRING), - SPLIT_PART("split_part", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT), - STARTS_WITH("starts_with", CnosDBDataType.BOOLEAN, CnosDBDataType.STRING, CnosDBDataType.STRING), - STRPOS("strpos", CnosDBDataType.INT, CnosDBDataType.STRING, CnosDBDataType.STRING), - SUBSTR("substr", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.INT, CnosDBDataType.INT), - TRANSLATE("translate", CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING, CnosDBDataType.STRING), - MD5("md5", CnosDBDataType.STRING, CnosDBDataType.STRING), - // mathematical functions - ABS("abs", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - CEIL("ceil", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - EXP("exp", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), LN("ln", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - LOG2("log2", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - LOG10("log10", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - POWER("power", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - ROUND("round", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - TRUNC("trunc", CnosDBDataType.DOUBLE, CnosDBDataType.INT), - FLOOR("floor", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - SIGNUM("signum", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - ACOS("acos", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - ASIN("asin", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - ATAN2("atan2", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - COS("cos", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), SIN("sin", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - SQRT("sqrt", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - TAN("tan", CnosDBDataType.DOUBLE, CnosDBDataType.DOUBLE), - DATE_PART("date_part", CnosDBDataType.INT, CnosDBDataType.STRING, CnosDBDataType.TIMESTAMP), - TO_TIMESTAMP("to_timestamp", CnosDBDataType.TIMESTAMP, CnosDBDataType.INT), - TO_TIMESTAMP_MILLIS("to_timestamp_millis", CnosDBDataType.TIMESTAMP, CnosDBDataType.INT), - TO_TIMESTAMP_MICROS("to_timestamp_micros", CnosDBDataType.TIMESTAMP, CnosDBDataType.INT), - TO_TIMESTAMP_SECONDS("to_timestamp_seconds", CnosDBDataType.TIMESTAMP, CnosDBDataType.INT); - - private final String functionName; - private final CnosDBDataType returnType; - private final CnosDBDataType[] argTypes; - - CnosDBFunctionWithUnknownResult(String functionName, CnosDBDataType returnType, CnosDBDataType... indexType) { - this.functionName = functionName; - this.returnType = returnType; - this.argTypes = indexType.clone(); - - } - - public static List getSupportedFunctions(CnosDBDataType type) { - List res = Stream.of(values()) - .filter(function -> function.isCompatibleWithReturnType(type)).collect(Collectors.toList()); - if (CnosDBBugs.BUG3547) { - res.removeAll(List.of(TO_TIMESTAMP, TO_TIMESTAMP_MICROS, TO_TIMESTAMP_MILLIS, TO_TIMESTAMP_SECONDS)); - } - return res; - } - - public boolean isCompatibleWithReturnType(CnosDBDataType t) { - return t == returnType; - } - - public CnosDBExpression[] getArguments(CnosDBDataType ignore, CnosDBExpressionGenerator gen, int depth) { - CnosDBExpression[] args = new CnosDBExpression[argTypes.length]; - for (int i = 0; i < args.length; i++) { - args[i] = gen.generateExpression(depth, argTypes[i]); - } - return args; - } - - public String getName() { - return functionName; - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBInOperation.java b/src/sqlancer/cnosdb/ast/CnosDBInOperation.java deleted file mode 100644 index c0ffd34ed..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBInOperation.java +++ /dev/null @@ -1,35 +0,0 @@ -package sqlancer.cnosdb.ast; - -import java.util.List; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public class CnosDBInOperation implements CnosDBExpression { - - private final CnosDBExpression expr; - private final List listElements; - private final boolean isTrue; - - public CnosDBInOperation(CnosDBExpression expr, List listElements, boolean isTrue) { - this.expr = expr; - this.listElements = listElements; - this.isTrue = isTrue; - } - - public CnosDBExpression getExpr() { - return expr; - } - - public List getListElements() { - return listElements; - } - - public boolean isTrue() { - return isTrue; - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBJoin.java b/src/sqlancer/cnosdb/ast/CnosDBJoin.java deleted file mode 100644 index eea88466f..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBJoin.java +++ /dev/null @@ -1,46 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public class CnosDBJoin implements CnosDBExpression { - - private final CnosDBExpression tableReference; - private final CnosDBExpression onClause; - private final CnosDBJoinType type; - - public CnosDBJoin(CnosDBExpression tableReference, CnosDBExpression onClause, CnosDBJoinType type) { - this.tableReference = tableReference; - this.onClause = onClause; - this.type = type; - } - - public CnosDBExpression getTableReference() { - return tableReference; - } - - public CnosDBExpression getOnClause() { - return onClause; - } - - public CnosDBJoinType getType() { - return type; - } - - @Override - public CnosDBDataType getExpressionType() { - throw new AssertionError(); - } - - public enum CnosDBJoinType { - INNER, LEFT, RIGHT, FULL; - // now not support - // CROSS; - - public static CnosDBJoinType getRandom() { - return Randomly.fromOptions(values()); - } - - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBLikeOperation.java b/src/sqlancer/cnosdb/ast/CnosDBLikeOperation.java deleted file mode 100644 index 616cd39ee..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBLikeOperation.java +++ /dev/null @@ -1,22 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.common.ast.BinaryNode; - -public class CnosDBLikeOperation extends BinaryNode implements CnosDBExpression { - - public CnosDBLikeOperation(CnosDBExpression left, CnosDBExpression right) { - super(left, right); - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } - - @Override - public String getOperatorRepresentation() { - return "LIKE"; - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBOrderByTerm.java b/src/sqlancer/cnosdb/ast/CnosDBOrderByTerm.java deleted file mode 100644 index de5812d76..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBOrderByTerm.java +++ /dev/null @@ -1,37 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public class CnosDBOrderByTerm implements CnosDBExpression { - - private final CnosDBOrder order; - private final CnosDBExpression expr; - - public CnosDBOrderByTerm(CnosDBExpression expr, CnosDBOrder order) { - this.expr = expr; - this.order = order; - } - - public CnosDBOrder getOrder() { - return order; - } - - public CnosDBExpression getExpr() { - return expr; - } - - @Override - public CnosDBDataType getExpressionType() { - return null; - } - - public enum CnosDBOrder { - ASC, DESC; - - public static CnosDBOrder getRandomOrder() { - return Randomly.fromOptions(CnosDBOrder.values()); - } - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBPostfixOperation.java b/src/sqlancer/cnosdb/ast/CnosDBPostfixOperation.java deleted file mode 100644 index f37621f44..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBPostfixOperation.java +++ /dev/null @@ -1,97 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.common.ast.BinaryOperatorNode.Operator; - -public class CnosDBPostfixOperation implements CnosDBExpression { - - private final CnosDBExpression expr; - private final String operatorTextRepresentation; - - public CnosDBPostfixOperation(CnosDBExpression expr, PostfixOperator op) { - this.expr = expr; - this.operatorTextRepresentation = Randomly.fromOptions(op.textRepresentations); - } - - public static CnosDBExpression create(CnosDBExpression expr, PostfixOperator op) { - return new CnosDBPostfixOperation(expr, op); - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } - - public String getOperatorTextRepresentation() { - return operatorTextRepresentation; - } - - public CnosDBExpression getExpression() { - return expr; - } - - public enum PostfixOperator implements Operator { - IS_NULL("IS NULL"/* , "ISNULL" */) { - @Override - public CnosDBDataType[] getInputDataTypes() { - return CnosDBDataType.values(); - } - - }, - IS_UNKNOWN("IS UNKNOWN") { - @Override - public CnosDBDataType[] getInputDataTypes() { - return new CnosDBDataType[] { CnosDBDataType.BOOLEAN }; - } - }, - - IS_NOT_NULL("IS NOT NULL"/* "NOTNULL" */) { - - @Override - public CnosDBDataType[] getInputDataTypes() { - return CnosDBDataType.values(); - } - - }, - IS_NOT_UNKNOWN("IS NOT UNKNOWN") { - - @Override - public CnosDBDataType[] getInputDataTypes() { - return new CnosDBDataType[] { CnosDBDataType.BOOLEAN }; - } - }, - IS_TRUE("IS TRUE") { - @Override - public CnosDBDataType[] getInputDataTypes() { - return new CnosDBDataType[] { CnosDBDataType.BOOLEAN }; - } - - }, - IS_FALSE("IS FALSE") { - @Override - public CnosDBDataType[] getInputDataTypes() { - return new CnosDBDataType[] { CnosDBDataType.BOOLEAN }; - } - - }; - - private final String[] textRepresentations; - - PostfixOperator(String... textRepresentations) { - this.textRepresentations = textRepresentations.clone(); - } - - public static PostfixOperator getRandom() { - return Randomly.fromOptions(values()); - } - - public abstract CnosDBDataType[] getInputDataTypes(); - - @Override - public String getTextRepresentation() { - return toString(); - } - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBPostfixText.java b/src/sqlancer/cnosdb/ast/CnosDBPostfixText.java deleted file mode 100644 index 241fab89a..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBPostfixText.java +++ /dev/null @@ -1,29 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public class CnosDBPostfixText implements CnosDBExpression { - - private final CnosDBExpression expr; - private final String text; - private final CnosDBDataType type; - - public CnosDBPostfixText(CnosDBExpression expr, String text, CnosDBDataType type) { - this.expr = expr; - this.text = text; - this.type = type; - } - - public CnosDBExpression getExpr() { - return expr; - } - - public String getText() { - return text; - } - - @Override - public CnosDBDataType getExpressionType() { - return type; - } -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBPrefixOperation.java b/src/sqlancer/cnosdb/ast/CnosDBPrefixOperation.java deleted file mode 100644 index db37f0089..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBPrefixOperation.java +++ /dev/null @@ -1,73 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.common.ast.BinaryOperatorNode.Operator; - -public class CnosDBPrefixOperation implements CnosDBExpression { - - private final CnosDBExpression expr; - private final PrefixOperator op; - - public CnosDBPrefixOperation(CnosDBExpression expr, PrefixOperator op) { - this.expr = expr; - this.op = op; - } - - @Override - public CnosDBDataType getExpressionType() { - return op.getExpressionType(); - } - - public CnosDBDataType[] getInputDataTypes() { - return op.dataTypes; - } - - public String getTextRepresentation() { - return op.textRepresentation; - } - - public CnosDBExpression getExpression() { - return expr; - } - - public enum PrefixOperator implements Operator { - NOT("NOT", CnosDBDataType.BOOLEAN) { - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } - - }, - UNARY_PLUS("+", CnosDBDataType.INT) { - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.INT; - } - - }, - UNARY_MINUS("-", CnosDBDataType.INT) { - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.INT; - } - - }; - - private final String textRepresentation; - private final CnosDBDataType[] dataTypes; - - PrefixOperator(String textRepresentation, CnosDBDataType... dataTypes) { - this.textRepresentation = textRepresentation; - this.dataTypes = dataTypes.clone(); - } - - public abstract CnosDBDataType getExpressionType(); - - @Override - public String getTextRepresentation() { - return toString(); - } - - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBSelect.java b/src/sqlancer/cnosdb/ast/CnosDBSelect.java deleted file mode 100644 index 0db657f19..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBSelect.java +++ /dev/null @@ -1,102 +0,0 @@ -package sqlancer.cnosdb.ast; - -import java.util.Collections; -import java.util.List; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.CnosDBSchema.CnosDBTable; -import sqlancer.common.ast.SelectBase; - -public class CnosDBSelect extends SelectBase implements CnosDBExpression { - - private SelectType selectOption = SelectType.ALL; - private List joinClauses = Collections.emptyList(); - private CnosDBExpression distinctOnClause; - - public void setSelectType(SelectType fromOptions) { - this.setSelectOption(fromOptions); - } - - public SelectType getSelectOption() { - return selectOption; - } - - public void setSelectOption(SelectType fromOptions) { - this.selectOption = fromOptions; - } - - @Override - public CnosDBDataType getExpressionType() { - return null; - } - - public List getJoinClauses() { - return joinClauses; - } - - public void setJoinClauses(List joinStatements) { - this.joinClauses = joinStatements; - - } - - public CnosDBExpression getDistinctOnClause() { - return distinctOnClause; - } - - public void setDistinctOnClause(CnosDBExpression distinctOnClause) { - if (selectOption != SelectType.DISTINCT) { - throw new IllegalArgumentException(); - } - this.distinctOnClause = distinctOnClause; - } - - public enum SelectType { - DISTINCT, ALL; - - public static SelectType getRandom() { - return Randomly.fromOptions(values()); - } - } - - public static class CnosDBFromTable implements CnosDBExpression { - private final CnosDBTable t; - - public CnosDBFromTable(CnosDBTable t) { - this.t = t; - } - - public CnosDBTable getTable() { - return t; - } - - @Override - public CnosDBDataType getExpressionType() { - return null; - } - } - - public static class CnosDBSubquery implements CnosDBExpression { - private final CnosDBSelect s; - private final String name; - - public CnosDBSubquery(CnosDBSelect s, String name) { - this.s = s; - this.name = name; - } - - public CnosDBSelect getSelect() { - return s; - } - - public String getName() { - return name; - } - - @Override - public CnosDBDataType getExpressionType() { - return null; - } - } - -} diff --git a/src/sqlancer/cnosdb/ast/CnosDBSimilarTo.java b/src/sqlancer/cnosdb/ast/CnosDBSimilarTo.java deleted file mode 100644 index 9e3467ada..000000000 --- a/src/sqlancer/cnosdb/ast/CnosDBSimilarTo.java +++ /dev/null @@ -1,28 +0,0 @@ -package sqlancer.cnosdb.ast; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public class CnosDBSimilarTo implements CnosDBExpression { - - private final CnosDBExpression string; - private final CnosDBExpression similarTo; - - public CnosDBSimilarTo(CnosDBExpression string, CnosDBExpression similarTo) { - this.string = string; - this.similarTo = similarTo; - } - - public CnosDBExpression getString() { - return string; - } - - public CnosDBExpression getSimilarTo() { - return similarTo; - } - - @Override - public CnosDBDataType getExpressionType() { - return CnosDBDataType.BOOLEAN; - } - -} diff --git a/src/sqlancer/cnosdb/client/CnosDBClient.java b/src/sqlancer/cnosdb/client/CnosDBClient.java deleted file mode 100644 index ccc9dcc16..000000000 --- a/src/sqlancer/cnosdb/client/CnosDBClient.java +++ /dev/null @@ -1,110 +0,0 @@ -package sqlancer.cnosdb.client; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; - -import org.apache.commons.codec.binary.Base64; -import org.apache.http.HttpHeaders; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; - -import com.arangodb.internal.util.IOUtils; - -public class CnosDBClient { - private final String userName; - private final String password; - private final String host; - private final int port; - - private final String database; - private final CloseableHttpClient client; - - public CnosDBClient(String host, int port, String userName, String password, String database) { - this.host = host; - this.port = port; - this.userName = userName; - this.password = password; - this.database = database; - this.client = HttpClientBuilder.create().build(); - } - - private String url() { - return "http://" + host + ":" + port + "/api/v1/"; - } - - public String ping() throws Exception { - HttpGet httpGet = new HttpGet(this.url() + "ping"); - httpGet.setHeader(HttpHeaders.AUTHORIZATION, getAuth()); - CloseableHttpResponse resp = client.execute(httpGet); - - String content = IOUtils.toString(resp.getEntity().getContent()); - resp.close(); - return content; - } - - public CnosDBResultSet executeQuery(String query) throws Exception { - HttpUriRequest request = createRequest(query); - CloseableHttpResponse resp = client.execute(request); - String text = IOUtils.toString(resp.getEntity().getContent()); - if (resp.getStatusLine().getStatusCode() != 200) { - resp.close(); - throw new CnosDBException(database + ":" + query + ";\n" + text); - } - resp.close(); - InputStream stream = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)); - - return new CnosDBResultSet(new InputStreamReader(stream)); - } - - public boolean execute(String query) throws Exception { - HttpUriRequest request = createRequest(query); - CloseableHttpResponse resp = client.execute(request); - if (resp.getStatusLine().getStatusCode() != 200) { - String res = IOUtils.toString(resp.getEntity().getContent()); - resp.close(); - throw new CnosDBException(query + res); - } - resp.close(); - return true; - } - - public void close() throws IOException { - client.close(); - } - - public String getDatabase() { - return this.database; - } - - private String getAuth() { - String auth = userName + ":" + password; - byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.ISO_8859_1)); - return "Basic " + new String(encodedAuth); - - } - - private HttpUriRequest createRequest(String query) throws URISyntaxException, UnsupportedEncodingException { - - URIBuilder builder = new URIBuilder(this.url() + "sql"); - builder.setParameter("db", database); - builder.setParameter("pretty", "true"); - HttpPost httpPost = new HttpPost(builder.build()); - - httpPost.setHeader(HttpHeaders.AUTHORIZATION, getAuth()); - StringEntity stringEntity = new StringEntity(query); - httpPost.setEntity(stringEntity); - return httpPost; - } - -} diff --git a/src/sqlancer/cnosdb/client/CnosDBConnection.java b/src/sqlancer/cnosdb/client/CnosDBConnection.java deleted file mode 100644 index 9277f203b..000000000 --- a/src/sqlancer/cnosdb/client/CnosDBConnection.java +++ /dev/null @@ -1,27 +0,0 @@ -package sqlancer.cnosdb.client; - -import java.io.IOException; - -import sqlancer.SQLancerDBConnection; - -public class CnosDBConnection implements SQLancerDBConnection { - private final CnosDBClient client; - - public CnosDBConnection(CnosDBClient client) { - this.client = client; - } - - @Override - public String getDatabaseVersion() throws Exception { - return client.ping(); - } - - public CnosDBClient getClient() { - return client; - } - - @Override - public void close() throws IOException { - client.close(); - } -} diff --git a/src/sqlancer/cnosdb/client/CnosDBException.java b/src/sqlancer/cnosdb/client/CnosDBException.java deleted file mode 100644 index a1055e90b..000000000 --- a/src/sqlancer/cnosdb/client/CnosDBException.java +++ /dev/null @@ -1,9 +0,0 @@ -package sqlancer.cnosdb.client; - -public class CnosDBException extends RuntimeException { - private static final long serialVersionUID = 1L; - - CnosDBException(String message) { - super(message); - } -} diff --git a/src/sqlancer/cnosdb/client/CnosDBResultSet.java b/src/sqlancer/cnosdb/client/CnosDBResultSet.java deleted file mode 100644 index 877b6ba5d..000000000 --- a/src/sqlancer/cnosdb/client/CnosDBResultSet.java +++ /dev/null @@ -1,52 +0,0 @@ -package sqlancer.cnosdb.client; - -import java.io.Reader; -import java.sql.SQLException; -import java.util.Iterator; - -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVRecord; - -import sqlancer.IgnoreMeException; - -public class CnosDBResultSet { - private final Iterator records; - private CSVRecord next; - - public CnosDBResultSet(Reader in) throws Exception { - Iterable records = CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build() - .parse(in); - this.records = records.iterator(); - } - - public void close() { - } - - public boolean next() throws SQLException { - if (records.hasNext()) { - next = records.next(); - return true; - } - return false; - } - - public int getInt(int i) throws SQLException { - return Integer.parseInt(next.get(i - 1)); - } - - public String getString(int i) throws SQLException { - return next.get(i - 1); - } - - public long getLong(int i) throws SQLException { - if (next.get(i - 1).isEmpty()) { - throw new IgnoreMeException(); - } - return Long.parseLong(next.get(i - 1)); - } - - // public boolean getBool(int i) throws Exception { - // return Boolean.parseBoolean(getString(i)); - // } - -} diff --git a/src/sqlancer/cnosdb/gen/CnosDBCommon.java b/src/sqlancer/cnosdb/gen/CnosDBCommon.java deleted file mode 100644 index 6c7b0bba7..000000000 --- a/src/sqlancer/cnosdb/gen/CnosDBCommon.java +++ /dev/null @@ -1,31 +0,0 @@ -package sqlancer.cnosdb.gen; - -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; - -public final class CnosDBCommon { - - private CnosDBCommon() { - } - - public static void appendDataType(CnosDBDataType type, StringBuilder sb) throws AssertionError { - switch (type) { - case BOOLEAN: - sb.append("BOOLEAN"); - break; - case INT: - sb.append("BIGINT"); - break; - case STRING: - sb.append("STRING"); - break; - case DOUBLE: - sb.append("DOUBLE"); - break; - case UINT: - sb.append("BIGINT UNSIGNED"); - break; - default: - throw new AssertionError(type); - } - } -} diff --git a/src/sqlancer/cnosdb/gen/CnosDBExpressionGenerator.java b/src/sqlancer/cnosdb/gen/CnosDBExpressionGenerator.java deleted file mode 100644 index 121f78254..000000000 --- a/src/sqlancer/cnosdb/gen/CnosDBExpressionGenerator.java +++ /dev/null @@ -1,461 +0,0 @@ -package sqlancer.cnosdb.gen; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import sqlancer.IgnoreMeException; -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBCompoundDataType; -import sqlancer.cnosdb.CnosDBGlobalState; -import sqlancer.cnosdb.CnosDBSchema.CnosDBColumn; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.ast.CnosDBAggregate; -import sqlancer.cnosdb.ast.CnosDBAggregate.CnosDBAggregateFunction; -import sqlancer.cnosdb.ast.CnosDBBetweenOperation; -import sqlancer.cnosdb.ast.CnosDBBinaryArithmeticOperation; -import sqlancer.cnosdb.ast.CnosDBBinaryArithmeticOperation.CnosDBBinaryOperator; -import sqlancer.cnosdb.ast.CnosDBBinaryComparisonOperation; -import sqlancer.cnosdb.ast.CnosDBBinaryLogicalOperation; -import sqlancer.cnosdb.ast.CnosDBBinaryLogicalOperation.BinaryLogicalOperator; -import sqlancer.cnosdb.ast.CnosDBCastOperation; -import sqlancer.cnosdb.ast.CnosDBColumnValue; -import sqlancer.cnosdb.ast.CnosDBConcatOperation; -import sqlancer.cnosdb.ast.CnosDBConstant; -import sqlancer.cnosdb.ast.CnosDBExpression; -import sqlancer.cnosdb.ast.CnosDBFunction; -import sqlancer.cnosdb.ast.CnosDBFunctionWithUnknownResult; -import sqlancer.cnosdb.ast.CnosDBInOperation; -import sqlancer.cnosdb.ast.CnosDBLikeOperation; -import sqlancer.cnosdb.ast.CnosDBOrderByTerm; -import sqlancer.cnosdb.ast.CnosDBOrderByTerm.CnosDBOrder; -import sqlancer.cnosdb.ast.CnosDBPostfixOperation; -import sqlancer.cnosdb.ast.CnosDBPostfixOperation.PostfixOperator; -import sqlancer.cnosdb.ast.CnosDBPrefixOperation; -import sqlancer.cnosdb.ast.CnosDBPrefixOperation.PrefixOperator; -import sqlancer.cnosdb.ast.CnosDBSimilarTo; -import sqlancer.common.gen.ExpressionGenerator; - -public class CnosDBExpressionGenerator implements ExpressionGenerator { - - private final int maxDepth; - - private final Randomly r; - - private List columns; - - private boolean allowAggregateFunctions; - - public CnosDBExpressionGenerator(CnosDBGlobalState globalState) { - this.r = globalState.getRandomly(); - this.maxDepth = globalState.getOptions().getMaxExpressionDepth(); - } - - public static CnosDBExpression generateExpression(CnosDBGlobalState globalState, CnosDBDataType type) { - return new CnosDBExpressionGenerator(globalState).generateExpression(0, type); - } - - private static CnosDBCompoundDataType getCompoundDataType(CnosDBDataType type) { - return CnosDBCompoundDataType.create(type); - } - - public static CnosDBExpression generateConstant(Randomly r, CnosDBDataType type) { - if (Randomly.getBooleanWithSmallProbability()) { - return CnosDBConstant.createNullConstant(); - } - switch (type) { - case INT: - return CnosDBConstant.createIntConstant(r.getInteger()); - case UINT: - return CnosDBConstant.createUintConstant(r.getPositiveInteger()); - case TIMESTAMP: - return CnosDBConstant.createTimeStampConstant(r.getPositiveIntegerNotNull()); - case BOOLEAN: - return CnosDBConstant.createBooleanConstant(Randomly.getBoolean()); - case STRING: - return CnosDBConstant.createStringConstant(r.getString()); - case DOUBLE: - return CnosDBConstant.createDoubleConstant(r.getDouble()); - default: - throw new AssertionError(type); - } - } - - public static CnosDBExpression generateExpression(CnosDBGlobalState globalState, List columns, - CnosDBDataType type) { - return new CnosDBExpressionGenerator(globalState).setColumns(columns).generateExpression(0, type); - } - - public static CnosDBExpression generateExpression(CnosDBGlobalState globalState, List columns) { - return new CnosDBExpressionGenerator(globalState).setColumns(columns).generateExpression(0); - } - - public CnosDBExpressionGenerator setColumns(List columns) { - this.columns = columns; - return this; - } - - public CnosDBExpression generateExpression(int depth) { - return generateExpression(depth, CnosDBDataType.getRandomType()); - } - - public List generateOrderBy() { - List orderBys = new ArrayList<>(); - for (int i = 0; i < Randomly.smallNumber(); i++) { - orderBys.add(new CnosDBOrderByTerm(CnosDBColumnValue.create(Randomly.fromList(columns)), - CnosDBOrder.getRandomOrder())); - } - return orderBys; - } - - private CnosDBExpression generateFunctionWithUnknownResult(int depth, CnosDBDataType type) { - List supportedFunctions = CnosDBFunctionWithUnknownResult - .getSupportedFunctions(type); - if (supportedFunctions.isEmpty()) { - throw new IgnoreMeException(); - } - CnosDBFunctionWithUnknownResult randomFunction = Randomly.fromList(supportedFunctions); - return new CnosDBFunction(randomFunction, type, randomFunction.getArguments(type, this, depth + 1)); - } - - private CnosDBExpression generateBooleanExpression(int depth) { - List validOptions = new ArrayList<>(Arrays.asList(BooleanExpression.values())); - BooleanExpression option = Randomly.fromList(validOptions); - switch (option) { - case POSTFIX_OPERATOR: - PostfixOperator random = PostfixOperator.getRandom(); - return CnosDBPostfixOperation - .create(generateExpression(depth + 1, Randomly.fromOptions(random.getInputDataTypes())), random); - case IN_OPERATION: - return inOperation(depth + 1); - case NOT: - return new CnosDBPrefixOperation(generateExpression(depth + 1, CnosDBDataType.BOOLEAN), PrefixOperator.NOT); - case BINARY_LOGICAL_OPERATOR: - CnosDBExpression first = generateExpression(depth + 1, CnosDBDataType.BOOLEAN); - int nr = Randomly.smallNumber() + 1; - for (int i = 0; i < nr; i++) { - first = new CnosDBBinaryLogicalOperation(first, generateExpression(depth + 1, CnosDBDataType.BOOLEAN), - BinaryLogicalOperator.getRandom()); - } - return first; - case BINARY_COMPARISON: - CnosDBDataType dataType = getMeaningfulType(); - return generateComparison(depth, dataType); - case CAST: - return generateCastExpression(depth + 1, CnosDBDataType.BOOLEAN); - case FUNCTION: - return generateFunction(depth + 1, CnosDBDataType.BOOLEAN); - case LIKE: - return new CnosDBLikeOperation(generateExpression(depth + 1, CnosDBDataType.STRING), - generateExpression(depth + 1, CnosDBDataType.STRING)); - case BETWEEN: - CnosDBDataType type = getMeaningfulType(); - return new CnosDBBetweenOperation(generateExpression(depth + 1, type), generateExpression(depth + 1, type), - generateExpression(depth + 1, type)); - case SIMILAR_TO: - return new CnosDBSimilarTo(generateExpression(depth + 1, CnosDBDataType.STRING), - generateExpression(depth + 1, CnosDBDataType.STRING)); - default: - throw new AssertionError(); - } - } - - private CnosDBDataType getMeaningfulType() { - // make it more likely that the expression does not only consist of constant - // expressions - if (Randomly.getBooleanWithSmallProbability() || columns == null || columns.isEmpty()) { - return CnosDBDataType.getRandomType(); - } else { - return Randomly.fromList(columns).getType(); - } - } - - private CnosDBExpression generateFunction(int depth, CnosDBDataType type) { - return generateFunctionWithUnknownResult(depth, type); - } - - private CnosDBExpression generateComparison(int depth, CnosDBDataType dataType) { - CnosDBExpression leftExpr = generateExpression(depth + 1, dataType); - CnosDBExpression rightExpr = generateExpression(depth + 1, dataType); - return getComparison(leftExpr, rightExpr); - } - - private CnosDBExpression getComparison(CnosDBExpression leftExpr, CnosDBExpression rightExpr) { - return new CnosDBBinaryComparisonOperation(leftExpr, rightExpr, - CnosDBBinaryComparisonOperation.CnosDBBinaryComparisonOperator.getRandom()); - } - - private CnosDBExpression inOperation(int depth) { - CnosDBDataType type = CnosDBDataType.getRandomType(); - CnosDBExpression leftExpr = generateExpression(depth + 1, type); - List rightExpr = new ArrayList<>(); - for (int i = 0; i < Randomly.smallNumber() + 1; i++) { - rightExpr.add(generateConstant(new Randomly(), type)); - } - return new CnosDBInOperation(leftExpr, rightExpr, Randomly.getBoolean()); - } - - public CnosDBExpression generateExpression(int depth, CnosDBDataType originalType) { - return generateExpressionInternal(depth, originalType); - } - - private CnosDBExpression generateExpressionInternal(int depth, CnosDBDataType dataType) throws AssertionError { - if (allowAggregateFunctions && Randomly.getBoolean()) { - return getAggregate(dataType); - } - - if (Randomly.getBooleanWithRatherLowProbability() || depth > maxDepth) { - // generic expression - if (Randomly.getBoolean() || depth > maxDepth) { - if (Randomly.getBooleanWithRatherLowProbability()) { - return generateConstant(r, dataType); - } else { - if (filterColumns(dataType).isEmpty()) { - return generateConstant(r, dataType); - } else { - return createColumnOfType(dataType); - } - } - } else { - if (Randomly.getBoolean()) { - return generateCastExpression(depth + 1, dataType); - } else { - return generateFunctionWithUnknownResult(depth, dataType); - } - } - } else { - switch (dataType) { - case BOOLEAN: - return generateBooleanExpression(depth); - case INT: - return generateIntExpression(depth); - case UINT: - return generateUIntExpression(depth); - case STRING: - return generateStringExpression(depth); - case DOUBLE: - return generateFloatExpression(depth); - case TIMESTAMP: - return generateTimeStampExpression(depth); - default: - throw new AssertionError(dataType); - } - } - } - - private CnosDBExpression generateStringExpression(int depth) { - StringExpression option; - List validOptions = new ArrayList<>(Arrays.asList(StringExpression.values())); - option = Randomly.fromList(validOptions); - - switch (option) { - case CAST: - return generateCastExpression(depth + 1, CnosDBDataType.STRING); - case FUNCTION: - return generateFunction(depth + 1, CnosDBDataType.STRING); - case CONCAT: - return generateConcat(depth); - default: - throw new AssertionError(); - } - } - - private CnosDBExpression generateConcat(int depth) { - CnosDBExpression left = generateExpression(depth + 1, CnosDBDataType.STRING); - CnosDBExpression right = generateExpression(depth + 1); - return new CnosDBConcatOperation(left, right); - } - - private CnosDBExpression generateIntExpression(int depth) { - IntExpression option; - option = Randomly.fromOptions(IntExpression.values()); - switch (option) { - case CAST: - return generateCastExpression(depth + 1, CnosDBDataType.INT); - case UNARY_OPERATION: - CnosDBExpression intExpression = generateExpression(depth + 1, CnosDBDataType.INT); - return new CnosDBPrefixOperation(intExpression, - Randomly.getBoolean() ? PrefixOperator.UNARY_PLUS : PrefixOperator.UNARY_MINUS); - case FUNCTION: - return generateFunction(depth + 1, CnosDBDataType.INT); - case BINARY_ARITHMETIC_EXPRESSION: - return new CnosDBBinaryArithmeticOperation(generateExpression(depth + 1, CnosDBDataType.INT), - generateExpression(depth + 1, CnosDBDataType.INT), - CnosDBBinaryOperator.getRandom(CnosDBDataType.INT)); - default: - throw new AssertionError(); - } - } - - private CnosDBExpression generateUIntExpression(int depth) { - UIntExpression option = Randomly.fromOptions(UIntExpression.values()); - switch (option) { - case CAST: - return generateCastExpression(depth + 1, CnosDBDataType.UINT); - case FUNCTION: - return generateFunction(depth + 1, CnosDBDataType.UINT); - case BINARY_ARITHMETIC_EXPRESSION: - return new CnosDBBinaryArithmeticOperation(generateExpression(depth + 1, CnosDBDataType.UINT), - generateExpression(depth + 1, CnosDBDataType.UINT), - CnosDBBinaryOperator.getRandom(CnosDBDataType.UINT)); - default: - throw new AssertionError(); - } - - } - - private CnosDBExpression generateFloatExpression(int depth) { - FloatExpression option; - option = Randomly.fromOptions(FloatExpression.values()); - switch (option) { - case CAST: - return generateCastExpression(depth + 1, CnosDBDataType.DOUBLE); - case UNARY_OPERATION: - CnosDBExpression floatExpression = generateExpression(depth + 1, CnosDBDataType.DOUBLE); - return new CnosDBPrefixOperation(floatExpression, - Randomly.getBoolean() ? PrefixOperator.UNARY_PLUS : PrefixOperator.UNARY_MINUS); - case FUNCTION: - return generateFunction(depth + 1, CnosDBDataType.DOUBLE); - case BINARY_ARITHMETIC_EXPRESSION: - return new CnosDBBinaryArithmeticOperation(generateExpression(depth + 1, CnosDBDataType.DOUBLE), - generateExpression(depth + 1, CnosDBDataType.DOUBLE), - CnosDBBinaryOperator.getRandom(CnosDBDataType.DOUBLE)); - case CONSTANT: - return generateConstant(r, CnosDBDataType.DOUBLE); - default: - throw new AssertionError(); - } - } - - private CnosDBExpression generateTimeStampExpression(int depth) { - if (Randomly.getBoolean()) { - return generateConstant(r, CnosDBDataType.TIMESTAMP); - } - TimestampExpression option; - option = Randomly.fromOptions(TimestampExpression.values()); - switch (option) { - case CAST: - return generateCastExpression(depth + 1, CnosDBDataType.TIMESTAMP); - case FUNCTION: - return generateFunction(depth + 1, CnosDBDataType.TIMESTAMP); - default: - throw new AssertionError(); - } - } - - private CnosDBExpression generateCastExpression(int depth, CnosDBDataType castToType) { - CnosDBDataType castFromType = Randomly.fromList(CnosDBCastOperation.canCastTo(castToType)); - return new CnosDBCastOperation(generateExpression(depth + 1, castFromType), getCompoundDataType(castToType)); - } - - private CnosDBExpression createColumnOfType(CnosDBDataType type) { - List columns = filterColumns(type); - if (columns.isEmpty()) { - throw new IgnoreMeException(); - } - CnosDBColumn fromList = Randomly.fromList(columns); - return CnosDBColumnValue.create(fromList); - } - - final List filterColumns(CnosDBDataType type) { - if (columns == null) { - return Collections.emptyList(); - } else { - return columns.stream().filter(c -> c.getType() == type).collect(Collectors.toList()); - } - } - - public List generateExpressions(int nr) { - List expressions = new ArrayList<>(); - for (int i = 0; i < nr; i++) { - expressions.add(generateExpression(0)); - } - return expressions; - } - - public CnosDBExpression generateExpression(CnosDBDataType dataType) { - return generateExpression(0, dataType); - } - - public CnosDBExpression generateHavingClause() { - this.allowAggregateFunctions = true; - CnosDBExpression expression = generateExpression(CnosDBDataType.BOOLEAN); - this.allowAggregateFunctions = false; - return expression; - } - - public CnosDBExpression generateAggregate() { - return getAggregate(CnosDBDataType.getRandomType()); - } - - private CnosDBExpression getAggregate(CnosDBDataType dataType) { - if (dataType == CnosDBDataType.BOOLEAN) { - List aggregates = CnosDBAggregateFunction.getAggregates(CnosDBDataType.INT); - CnosDBAggregateFunction agg = Randomly.fromList(aggregates); - return new CnosDBCastOperation(generateArgsForAggregate(dataType, agg), - CnosDBCompoundDataType.create(CnosDBDataType.BOOLEAN)); - } else { - List aggregates = CnosDBAggregateFunction.getAggregates(dataType); - CnosDBAggregateFunction agg = Randomly.fromList(aggregates); - return generateArgsForAggregate(dataType, agg); - } - } - - public CnosDBAggregate generateArgsForAggregate(CnosDBDataType dataType, CnosDBAggregateFunction agg) { - CnosDBDataType[] types = agg.getArgsTypes(dataType); - List args = new ArrayList<>(); - for (CnosDBDataType argType : types) { - args.add(createColumnOfType(argType)); - // args.add(generateExpression(argType)); - } - return new CnosDBAggregate(args, agg); - } - - public CnosDBExpressionGenerator allowAggregates(boolean value) { - allowAggregateFunctions = value; - return this; - } - - @Override - public CnosDBExpression generatePredicate() { - return generateExpression(CnosDBDataType.BOOLEAN); - } - - @Override - public CnosDBExpression negatePredicate(CnosDBExpression predicate) { - return new CnosDBPrefixOperation(predicate, PrefixOperator.NOT); - } - - @Override - public CnosDBExpression isNull(CnosDBExpression expr) { - return new CnosDBPostfixOperation(expr, PostfixOperator.IS_NULL); - } - - private enum BooleanExpression { - POSTFIX_OPERATOR, NOT, BINARY_LOGICAL_OPERATOR, BINARY_COMPARISON, FUNCTION, CAST, LIKE, BETWEEN, IN_OPERATION, - SIMILAR_TO, - } - - private enum StringExpression { - CAST, FUNCTION, CONCAT - } - - private enum IntExpression { - UNARY_OPERATION, FUNCTION, CAST, BINARY_ARITHMETIC_EXPRESSION - } - - private enum UIntExpression { - FUNCTION, CAST, BINARY_ARITHMETIC_EXPRESSION - } - - private enum FloatExpression { - UNARY_OPERATION, FUNCTION, CAST, BINARY_ARITHMETIC_EXPRESSION, CONSTANT - } - - private enum TimestampExpression { - FUNCTION, CAST - } - -} diff --git a/src/sqlancer/cnosdb/gen/CnosDBInsertGenerator.java b/src/sqlancer/cnosdb/gen/CnosDBInsertGenerator.java deleted file mode 100644 index 0d575d3c7..000000000 --- a/src/sqlancer/cnosdb/gen/CnosDBInsertGenerator.java +++ /dev/null @@ -1,59 +0,0 @@ -package sqlancer.cnosdb.gen; - -import java.util.List; -import java.util.stream.Collectors; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBGlobalState; -import sqlancer.cnosdb.CnosDBSchema.CnosDBColumn; -import sqlancer.cnosdb.CnosDBSchema.CnosDBTable; -import sqlancer.cnosdb.CnosDBVisitor; -import sqlancer.cnosdb.ast.CnosDBExpression; -import sqlancer.cnosdb.query.CnosDBOtherQuery; -import sqlancer.common.query.ExpectedErrors; -import sqlancer.common.schema.AbstractTableColumn; - -public final class CnosDBInsertGenerator { - - private CnosDBInsertGenerator() { - } - - public static CnosDBOtherQuery insert(CnosDBGlobalState globalState) { - CnosDBTable table = globalState.getSchema().getRandomTable(); - ExpectedErrors errors = new ExpectedErrors(); - errors.add("Column time cannot be null."); - StringBuilder sb = new StringBuilder(); - sb.append("INSERT "); - sb.append(table.getName()); - List columns = table.getRandomNonEmptyColumnSubset(); - sb.append("("); - sb.append(columns.stream().map(AbstractTableColumn::getName).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES"); - - int n = Randomly.smallNumber() + 1; - for (int i = 0; i < n; i++) { - if (i != 0) { - sb.append(", "); - } - insertRow(globalState, sb, columns); - } - - // error - return new CnosDBOtherQuery(sb.toString(), errors); - } - - private static void insertRow(CnosDBGlobalState globalState, StringBuilder sb, List columns) { - sb.append("("); - for (int i = 0; i < columns.size(); i++) { - if (i > 0) { - sb.append(", "); - } - CnosDBExpression generateConstant = CnosDBExpressionGenerator.generateConstant(globalState.getRandomly(), - columns.get(i).getType()); - sb.append(CnosDBVisitor.asString(generateConstant)); - } - sb.append(")"); - } - -} diff --git a/src/sqlancer/cnosdb/gen/CnosDBTableGenerator.java b/src/sqlancer/cnosdb/gen/CnosDBTableGenerator.java deleted file mode 100644 index c046ad3e9..000000000 --- a/src/sqlancer/cnosdb/gen/CnosDBTableGenerator.java +++ /dev/null @@ -1,77 +0,0 @@ -package sqlancer.cnosdb.gen; - -import java.util.ArrayList; -import java.util.List; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBSchema.CnosDBColumn; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.CnosDBSchema.CnosDBFieldColumn; -import sqlancer.cnosdb.CnosDBSchema.CnosDBTable; -import sqlancer.cnosdb.CnosDBSchema.CnosDBTagColumn; -import sqlancer.cnosdb.query.CnosDBOtherQuery; -import sqlancer.common.query.ExpectedErrors; - -public class CnosDBTableGenerator { - - protected final ExpectedErrors errors = new ExpectedErrors(); - private final String tableName; - private final StringBuilder sb = new StringBuilder(); - private final List columnsToBeAdd = new ArrayList<>(); - private CnosDBTable table; - - public CnosDBTableGenerator(String tableName) { - this.tableName = tableName; - } - - public static CnosDBOtherQuery generate(String tableName) { - return new CnosDBTableGenerator(tableName).generate(); - } - - protected CnosDBOtherQuery generate() { - table = new CnosDBTable(tableName, columnsToBeAdd); - - sb.append("CREATE TABLE"); - if (Randomly.getBoolean()) { - sb.append(" IF NOT EXISTS"); - } - sb.append(" "); - sb.append(tableName); - - sb.append("("); - for (int i = 0; i < Randomly.smallNumber() + 1; i++) { - String name = String.format("f%d", i); - createField(name); - sb.append(", "); - } - - sb.append("TAGS("); - for (int i = 0; i < Randomly.smallNumber() + 1; i++) { - if (i != 0) { - sb.append(", "); - } - String name = String.format("t%d", i); - createTag(name); - } - sb.append("))"); - return new CnosDBOtherQuery(sb.toString(), new ExpectedErrors()); - } - - private void createField(String name) throws AssertionError { - sb.append(name); - sb.append(" "); - CnosDBDataType type = CnosDBDataType.getRandomTypeWithoutTimeStamp(); - CnosDBCommon.appendDataType(type, sb); - CnosDBFieldColumn c = new CnosDBFieldColumn(name, type); - c.setTable(table); - sb.append(" "); - columnsToBeAdd.add(c); - } - - private void createTag(String name) { - sb.append(name); - CnosDBColumn column = new CnosDBTagColumn(name); - column.setTable(table); - columnsToBeAdd.add(column); - } -} diff --git a/src/sqlancer/cnosdb/oracle/CnosDBNoRECBase.java b/src/sqlancer/cnosdb/oracle/CnosDBNoRECBase.java deleted file mode 100644 index 472aa8f66..000000000 --- a/src/sqlancer/cnosdb/oracle/CnosDBNoRECBase.java +++ /dev/null @@ -1,23 +0,0 @@ -package sqlancer.cnosdb.oracle; - -import sqlancer.Main; -import sqlancer.MainOptions; -import sqlancer.cnosdb.CnosDBGlobalState; -import sqlancer.cnosdb.client.CnosDBConnection; -import sqlancer.common.oracle.TestOracle; - -public abstract class CnosDBNoRECBase implements TestOracle { - protected final CnosDBGlobalState state; - protected final Main.StateLogger logger; - protected final MainOptions options; - protected final CnosDBConnection con; - protected String optimizedQueryString; - protected String unoptimizedQueryString; - - public CnosDBNoRECBase(CnosDBGlobalState state) { - this.state = state; - this.con = state.getConnection(); - this.logger = state.getLogger(); - this.options = state.getOptions(); - } -} diff --git a/src/sqlancer/cnosdb/oracle/CnosDBNoRECOracle.java b/src/sqlancer/cnosdb/oracle/CnosDBNoRECOracle.java deleted file mode 100644 index 0c817c655..000000000 --- a/src/sqlancer/cnosdb/oracle/CnosDBNoRECOracle.java +++ /dev/null @@ -1,171 +0,0 @@ -package sqlancer.cnosdb.oracle; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import sqlancer.IgnoreMeException; -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBCompoundDataType; -import sqlancer.cnosdb.CnosDBExpectedError; -import sqlancer.cnosdb.CnosDBGlobalState; -import sqlancer.cnosdb.CnosDBSchema; -import sqlancer.cnosdb.CnosDBSchema.CnosDBColumn; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.CnosDBSchema.CnosDBTable; -import sqlancer.cnosdb.CnosDBSchema.CnosDBTables; -import sqlancer.cnosdb.CnosDBVisitor; -import sqlancer.cnosdb.ast.CnosDBCastOperation; -import sqlancer.cnosdb.ast.CnosDBColumnValue; -import sqlancer.cnosdb.ast.CnosDBExpression; -import sqlancer.cnosdb.ast.CnosDBJoin; -import sqlancer.cnosdb.ast.CnosDBJoin.CnosDBJoinType; -import sqlancer.cnosdb.ast.CnosDBPostfixText; -import sqlancer.cnosdb.ast.CnosDBSelect; -import sqlancer.cnosdb.ast.CnosDBSelect.CnosDBFromTable; -import sqlancer.cnosdb.ast.CnosDBSelect.CnosDBSubquery; -import sqlancer.cnosdb.ast.CnosDBSelect.SelectType; -import sqlancer.cnosdb.client.CnosDBResultSet; -import sqlancer.cnosdb.gen.CnosDBExpressionGenerator; -import sqlancer.cnosdb.oracle.tlp.CnosDBTLPBase; -import sqlancer.cnosdb.query.CnosDBSelectQuery; -import sqlancer.common.oracle.TestOracle; - -public class CnosDBNoRECOracle extends CnosDBNoRECBase implements TestOracle { - - private final CnosDBSchema s; - - public CnosDBNoRECOracle(CnosDBGlobalState globalState) { - super(globalState); - this.s = globalState.getSchema(); - } - - public static List getJoinStatements(CnosDBGlobalState globalState, List columns, - List tables) { - List joinStatements = new ArrayList<>(); - CnosDBExpressionGenerator gen = new CnosDBExpressionGenerator(globalState).setColumns(columns); - for (int i = 1; i < tables.size(); i++) { - CnosDBExpression joinClause = gen.generateExpression(CnosDBDataType.BOOLEAN); - CnosDBTable table = Randomly.fromList(tables); - tables.remove(table); - CnosDBJoinType options = CnosDBJoinType.getRandom(); - CnosDBJoin j = new CnosDBJoin(new CnosDBFromTable(table), joinClause, options); - joinStatements.add(j); - } - // JOIN subqueries - for (int i = 0; i < Randomly.smallNumber(); i++) { - CnosDBTables subqueryTables = globalState.getSchema().getRandomTableNonEmptyTables(); - CnosDBSubquery subquery = CnosDBTLPBase.createSubquery(globalState, String.format("sub%d", i), - subqueryTables); - CnosDBExpression joinClause = gen.generateExpression(CnosDBDataType.BOOLEAN); - CnosDBJoinType options = CnosDBJoinType.getRandom(); - CnosDBJoin j = new CnosDBJoin(subquery, joinClause, options); - joinStatements.add(j); - } - return joinStatements; - } - - @Override - public void check() throws Exception { - CnosDBTables randomTables = s.getRandomTableNonEmptyTables(); - List columns = randomTables.getColumns(); - CnosDBExpression randomWhereCondition = getRandomWhereCondition(columns); - List tables = randomTables.getTables(); - - List joinStatements = getJoinStatements(state, columns, tables); - List fromTables = tables.stream().map(CnosDBFromTable::new).collect(Collectors.toList()); - int secondCount = getUnoptimizedQueryCount(fromTables, randomWhereCondition, joinStatements); - int firstCount = getOptimizedQueryCount(fromTables, List.of(CnosDBColumn.createDummy("f0")), - randomWhereCondition, joinStatements); - if (firstCount == -1 || secondCount == -1) { - throw new IgnoreMeException(); - } - if (firstCount != secondCount) { - String queryFormatString = "-- %s;\n-- count: %d"; - String firstQueryStringWithCount = String.format(queryFormatString, optimizedQueryString, firstCount); - String secondQueryStringWithCount = String.format(queryFormatString, unoptimizedQueryString, secondCount); - state.getState().getLocalState() - .log(String.format("%s\n%s", firstQueryStringWithCount, secondQueryStringWithCount)); - String assertionMessage = String.format("the counts mismatch (%d and %d)!\n%s\n%s", firstCount, secondCount, - firstQueryStringWithCount, secondQueryStringWithCount); - throw new AssertionError(assertionMessage); - } - } - - private CnosDBExpression getRandomWhereCondition(List columns) { - return new CnosDBExpressionGenerator(state).setColumns(columns).generateExpression(CnosDBDataType.BOOLEAN); - } - - private int getUnoptimizedQueryCount(List fromTables, CnosDBExpression randomWhereCondition, - List joinStatements) throws Exception { - CnosDBSelect select = new CnosDBSelect(); - CnosDBCastOperation isTrue = new CnosDBCastOperation(randomWhereCondition, - CnosDBCompoundDataType.create(CnosDBDataType.INT)); - CnosDBPostfixText asText = new CnosDBPostfixText(isTrue, " as count", CnosDBDataType.INT); - select.setFetchColumns(List.of(asText)); - select.setFromList(fromTables); - select.setSelectType(SelectType.ALL); - select.setJoinClauses(joinStatements); - int secondCount = 0; - unoptimizedQueryString = "SELECT SUM(count) FROM (" + CnosDBVisitor.asString(select) + ") as res"; - if (options.logEachSelect()) { - logger.writeCurrent(unoptimizedQueryString); - } - CnosDBSelectQuery q = new CnosDBSelectQuery(unoptimizedQueryString, CnosDBExpectedError.expectedErrors()); - CnosDBResultSet rs; - try { - q.executeAndGet(state); - rs = q.getResultSet(); - } catch (Exception e) { - if (q.getExpectedErrors().errorIsExpected(e.getMessage())) { - throw new IgnoreMeException(); - } - throw new AssertionError(unoptimizedQueryString, e); - } - if (rs == null) { - return -1; - } - - if (rs.next()) { - secondCount += rs.getLong(1); - } - rs.close(); - return secondCount; - } - - private int getOptimizedQueryCount(List randomTables, List columns, - CnosDBExpression randomWhereCondition, List joinStatements) { - CnosDBSelect select = new CnosDBSelect(); - CnosDBColumnValue allColumns = new CnosDBColumnValue(Randomly.fromList(columns)); - select.setFetchColumns(List.of(allColumns)); - select.setFromList(randomTables); - select.setWhereClause(randomWhereCondition); - if (Randomly.getBooleanWithSmallProbability()) { - select.setOrderByClauses(new CnosDBExpressionGenerator(state).setColumns(columns).generateOrderBy()); - } - select.setSelectType(SelectType.ALL); - select.setJoinClauses(joinStatements); - int firstCount = 0; - optimizedQueryString = CnosDBVisitor.asString(select); - if (options.logEachSelect()) { - logger.writeCurrent(optimizedQueryString); - } - CnosDBSelectQuery query = new CnosDBSelectQuery(optimizedQueryString, CnosDBExpectedError.expectedErrors()); - CnosDBResultSet rs; - try { - query.executeAndGet(state); - rs = query.getResultSet(); - while (rs.next()) { - firstCount++; - } - } catch (Exception e) { - if (query.getExpectedErrors().errorIsExpected(e.getMessage())) { - throw new IgnoreMeException(); - } - - throw new IgnoreMeException(); - } - return firstCount; - } - -} diff --git a/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPAggregateOracle.java b/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPAggregateOracle.java deleted file mode 100644 index b51624a94..000000000 --- a/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPAggregateOracle.java +++ /dev/null @@ -1,176 +0,0 @@ -package sqlancer.cnosdb.oracle.tlp; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import sqlancer.ComparatorHelper; -import sqlancer.IgnoreMeException; -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBExpectedError; -import sqlancer.cnosdb.CnosDBGlobalState; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.CnosDBVisitor; -import sqlancer.cnosdb.ast.CnosDBAggregate; -import sqlancer.cnosdb.ast.CnosDBAggregate.CnosDBAggregateFunction; -import sqlancer.cnosdb.ast.CnosDBAlias; -import sqlancer.cnosdb.ast.CnosDBExpression; -import sqlancer.cnosdb.ast.CnosDBJoin; -import sqlancer.cnosdb.ast.CnosDBPostfixOperation; -import sqlancer.cnosdb.ast.CnosDBPostfixOperation.PostfixOperator; -import sqlancer.cnosdb.ast.CnosDBPrefixOperation; -import sqlancer.cnosdb.ast.CnosDBPrefixOperation.PrefixOperator; -import sqlancer.cnosdb.ast.CnosDBSelect; -import sqlancer.cnosdb.client.CnosDBResultSet; -import sqlancer.cnosdb.query.CnosDBSelectQuery; -import sqlancer.common.oracle.TestOracle; - -public class CnosDBTLPAggregateOracle extends CnosDBTLPBase implements TestOracle { - - private String firstResult; - private String secondResult; - private String originalQuery; - private String metamorphicQuery; - - public CnosDBTLPAggregateOracle(CnosDBGlobalState state) { - super(state); - } - - @Override - public void check() throws Exception { - super.check(); - aggregateCheck(); - } - - protected void aggregateCheck() { - CnosDBAggregateFunction aggregateFunction = Randomly.fromOptions(CnosDBAggregateFunction.MAX, - CnosDBAggregateFunction.MIN, CnosDBAggregateFunction.SUM); - - CnosDBAggregate aggregate = gen.generateArgsForAggregate(aggregateFunction.getRandomReturnType(), - aggregateFunction); - List fetchColumns = new ArrayList<>(); - fetchColumns.add(aggregate); - while (Randomly.getBooleanWithRatherLowProbability()) { - fetchColumns.add(gen.generateAggregate()); - } - select.setFetchColumns(fetchColumns); - if (Randomly.getBooleanWithRatherLowProbability()) { - select.setOrderByClauses(gen.generateOrderBy()); - } - originalQuery = CnosDBVisitor.asString(select); - firstResult = getAggregateResult(originalQuery); - metamorphicQuery = createMetamorphicUnionQuery(select, aggregate, select.getFromList()); - secondResult = getAggregateResult(metamorphicQuery); - - String queryFormatString = "-- %s;\n-- result: %s"; - String firstQueryString = String.format(queryFormatString, originalQuery, firstResult); - String secondQueryString = String.format(queryFormatString, metamorphicQuery, secondResult); - state.getState().getLocalState().log(String.format("%s\n%s", firstQueryString, secondQueryString)); - if (firstResult == null && secondResult != null || firstResult != null && secondResult == null - || firstResult != null && !firstResult.contentEquals(secondResult) - && !ComparatorHelper.isEqualDouble(firstResult, secondResult)) { - if (secondResult != null && secondResult.contains("Inf")) { - throw new IgnoreMeException(); // FIXME: average computation - } - String assertionMessage = String.format("%s: the results mismatch!\n%s\n%s", this.s.getDatabaseName(), - firstQueryString, secondQueryString); - throw new AssertionError(assertionMessage); - } - } - - private String createMetamorphicUnionQuery(CnosDBSelect select, CnosDBAggregate aggregate, - List from) { - String metamorphicQuery; - CnosDBExpression whereClause = gen.generateExpression(CnosDBDataType.BOOLEAN); - CnosDBExpression negatedClause = new CnosDBPrefixOperation(whereClause, PrefixOperator.NOT); - CnosDBExpression notNullClause = new CnosDBPostfixOperation(whereClause, PostfixOperator.IS_NULL); - List mappedAggregate = mapped(aggregate); - CnosDBSelect leftSelect = getSelect(mappedAggregate, from, whereClause, select.getJoinClauses()); - CnosDBSelect middleSelect = getSelect(mappedAggregate, from, negatedClause, select.getJoinClauses()); - CnosDBSelect rightSelect = getSelect(mappedAggregate, from, notNullClause, select.getJoinClauses()); - metamorphicQuery = "SELECT " + getOuterAggregateFunction(aggregate) + " FROM ("; - metamorphicQuery += CnosDBVisitor.asString(leftSelect) + " UNION ALL " + CnosDBVisitor.asString(middleSelect) - + " UNION ALL " + CnosDBVisitor.asString(rightSelect); - metamorphicQuery += ") as asdf"; - return metamorphicQuery; - } - - private String getAggregateResult(String queryString) { - // log TLP Aggregate SELECT queries on the current log file - if (state.getOptions().logEachSelect()) { - // TODO: refactor me - state.getLogger().writeCurrent(queryString); - try { - state.getLogger().getCurrentFileWriter().flush(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - String resultString = null; - - CnosDBSelectQuery q = new CnosDBSelectQuery(queryString, CnosDBExpectedError.expectedErrors()); - try { - q.executeAndGet(state); - CnosDBResultSet result = q.getResultSet(); - - if (result == null || !result.next()) { - throw new IgnoreMeException(); - } - - resultString = result.getString(1); - - } catch (Exception e) { - if (q.getExpectedErrors().errorIsExpected(e.getMessage())) { - throw new IgnoreMeException(); - } - } - - return resultString; - } - - private List mapped(CnosDBAggregate aggregate) { - switch (aggregate.getFunction()) { - case SUM: - case MAX: - case MIN: - return aliasArgs(List.of(aggregate)); - // now not support - // case COUNT: - // case AVG: - default: - throw new AssertionError(aggregate.getFunction()); - } - } - - private List aliasArgs(List originalAggregateArgs) { - List args = new ArrayList<>(); - int i = 0; - for (CnosDBExpression expr : originalAggregateArgs) { - args.add(new CnosDBAlias(expr, "agg" + i++)); - } - return args; - } - - private String getOuterAggregateFunction(CnosDBAggregate aggregate) { - if (Objects.requireNonNull(aggregate.getFunction()) == CnosDBAggregateFunction.COUNT) { - return CnosDBAggregateFunction.SUM + "(agg0)"; - } - return aggregate.getFunction() + "(agg0)"; - } - - private CnosDBSelect getSelect(List aggregates, List from, - CnosDBExpression whereClause, List joinList) { - CnosDBSelect leftSelect = new CnosDBSelect(); - leftSelect.setFetchColumns(aggregates); - leftSelect.setFromList(from); - leftSelect.setWhereClause(whereClause); - leftSelect.setJoinClauses(joinList); - if (Randomly.getBooleanWithSmallProbability()) { - leftSelect.setGroupByExpressions(gen.generateExpressions(Randomly.smallNumber() + 1)); - } - return leftSelect; - } - -} diff --git a/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPBase.java b/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPBase.java deleted file mode 100644 index bd7ba3b55..000000000 --- a/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPBase.java +++ /dev/null @@ -1,112 +0,0 @@ -package sqlancer.cnosdb.oracle.tlp; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBGlobalState; -import sqlancer.cnosdb.CnosDBSchema; -import sqlancer.cnosdb.CnosDBSchema.CnosDBColumn; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.CnosDBSchema.CnosDBTable; -import sqlancer.cnosdb.CnosDBSchema.CnosDBTables; -import sqlancer.cnosdb.ast.CnosDBColumnValue; -import sqlancer.cnosdb.ast.CnosDBConstant; -import sqlancer.cnosdb.ast.CnosDBExpression; -import sqlancer.cnosdb.ast.CnosDBJoin; -import sqlancer.cnosdb.ast.CnosDBSelect; -import sqlancer.cnosdb.ast.CnosDBSelect.CnosDBFromTable; -import sqlancer.cnosdb.ast.CnosDBSelect.CnosDBSubquery; -import sqlancer.cnosdb.gen.CnosDBExpressionGenerator; -import sqlancer.cnosdb.oracle.CnosDBNoRECOracle; -import sqlancer.common.gen.ExpressionGenerator; -import sqlancer.common.oracle.TernaryLogicPartitioningOracleBase; -import sqlancer.common.oracle.TestOracle; - -public class CnosDBTLPBase extends TernaryLogicPartitioningOracleBase - implements TestOracle { - - protected CnosDBSchema s; - protected CnosDBTables targetTables; - protected CnosDBExpressionGenerator gen; - protected CnosDBSelect select; - - public CnosDBTLPBase(CnosDBGlobalState state) { - super(state); - } - - public static CnosDBSubquery createSubquery(CnosDBGlobalState globalState, String name, CnosDBTables tables) { - List columns = new ArrayList<>(); - CnosDBExpressionGenerator gen = new CnosDBExpressionGenerator(globalState).setColumns(tables.getColumns()); - for (int i = 0; i < Randomly.smallNumber() + 1; i++) { - columns.add(gen.generateExpression(0)); - } - CnosDBSelect select = new CnosDBSelect(); - select.setFromList(tables.getTables().stream().map(CnosDBFromTable::new).collect(Collectors.toList())); - select.setFetchColumns(columns); - if (Randomly.getBoolean()) { - select.setWhereClause(gen.generateExpression(0, CnosDBDataType.BOOLEAN)); - } - if (Randomly.getBooleanWithRatherLowProbability()) { - select.setOrderByClauses(gen.generateOrderBy()); - } - if (Randomly.getBoolean()) { - select.setLimitClause(CnosDBConstant.createIntConstant(Randomly.getPositiveOrZeroNonCachedInteger())); - if (Randomly.getBoolean()) { - select.setOffsetClause(CnosDBConstant.createIntConstant(Randomly.getPositiveOrZeroNonCachedInteger())); - } - } - return new CnosDBSubquery(select, name); - } - - @Override - public void check() throws Exception { - s = state.getSchema(); - targetTables = s.getRandomTableNonEmptyTables(); - List tables = targetTables.getTables(); - List joins = getJoinStatements(targetTables.getColumns(), tables); - generateSelectBase(tables, joins); - } - - protected List getJoinStatements(List columns, List tables) { - return CnosDBNoRECOracle.getJoinStatements(state, columns, tables); - } - - protected void generateSelectBase(List tables, List joins) { - List tableList = tables.stream().map(CnosDBFromTable::new).collect(Collectors.toList()); - gen = new CnosDBExpressionGenerator(state).setColumns(targetTables.getColumns()); - initializeTernaryPredicateVariants(); - select = new CnosDBSelect(); - select.setFetchColumns(generateFetchColumns()); - select.setFromList(tableList); - select.setWhereClause(null); - select.setJoinClauses(joins); - } - - List generateFetchColumns() { - if (Randomly.getBooleanWithRatherLowProbability()) { - return List.of(new CnosDBColumnValue(CnosDBColumn.createDummy("*"))); - } - List fetchColumns = new ArrayList<>(); - List targetColumns = targetTables.getRandomColumnsWithOnlyOneField(); - - ArrayList columns = new ArrayList<>(); - targetColumns.forEach(column -> column.getTable().getColumns().stream() - .filter(field -> field instanceof CnosDBSchema.CnosDBFieldColumn).findFirst().ifPresent(columns::add)); - targetColumns.addAll(columns); - - targetColumns = targetColumns.stream().distinct().collect(Collectors.toList()); - - for (CnosDBColumn c : targetColumns) { - fetchColumns.add(new CnosDBColumnValue(c)); - } - return fetchColumns; - } - - @Override - protected ExpressionGenerator getGen() { - return gen; - } - -} diff --git a/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPHavingOracle.java b/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPHavingOracle.java deleted file mode 100644 index 283d59a23..000000000 --- a/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPHavingOracle.java +++ /dev/null @@ -1,65 +0,0 @@ -package sqlancer.cnosdb.oracle.tlp; - -import java.util.ArrayList; -import java.util.List; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBComparatorHelper; -import sqlancer.cnosdb.CnosDBExpectedError; -import sqlancer.cnosdb.CnosDBGlobalState; -import sqlancer.cnosdb.CnosDBSchema.CnosDBDataType; -import sqlancer.cnosdb.CnosDBVisitor; -import sqlancer.cnosdb.ast.CnosDBExpression; - -public class CnosDBTLPHavingOracle extends CnosDBTLPBase { - - public CnosDBTLPHavingOracle(CnosDBGlobalState state) { - super(state); - } - - @Override - public void check() throws Exception { - super.check(); - havingCheck(); - } - - protected void havingCheck() throws Exception { - if (Randomly.getBoolean()) { - select.setWhereClause(gen.generateExpression(CnosDBDataType.BOOLEAN)); - } - select.setGroupByExpressions(gen.generateExpressions(Randomly.smallNumber() + 1)); - select.setHavingClause(null); - String originalQueryString = CnosDBVisitor.asString(select); - List resultSet = CnosDBComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, - CnosDBExpectedError.expectedErrors(), state); - - boolean orderBy = Randomly.getBoolean(); - if (orderBy) { - select.setOrderByClauses(gen.generateOrderBy()); - } - select.setHavingClause(predicate); - String firstQueryString = CnosDBVisitor.asString(select); - select.setHavingClause(negatedPredicate); - String secondQueryString = CnosDBVisitor.asString(select); - select.setHavingClause(isNullPredicate); - String thirdQueryString = CnosDBVisitor.asString(select); - List combinedString = new ArrayList<>(); - List secondResultSet = CnosDBComparatorHelper.getCombinedResultSet(firstQueryString, secondQueryString, - thirdQueryString, combinedString, !orderBy, state, CnosDBExpectedError.expectedErrors()); - CnosDBComparatorHelper.assumeResultSetsAreEqual(resultSet, secondResultSet, originalQueryString, combinedString, - state); - } - - @Override - protected CnosDBExpression generatePredicate() { - return gen.generateHavingClause(); - } - - @Override - List generateFetchColumns() { - List expressions = gen.allowAggregates(true).generateExpressions(Randomly.smallNumber() + 1); - gen.allowAggregates(false); - return expressions; - } - -} diff --git a/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPWhereOracle.java b/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPWhereOracle.java deleted file mode 100644 index 8e118435d..000000000 --- a/src/sqlancer/cnosdb/oracle/tlp/CnosDBTLPWhereOracle.java +++ /dev/null @@ -1,46 +0,0 @@ -package sqlancer.cnosdb.oracle.tlp; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import sqlancer.Randomly; -import sqlancer.cnosdb.CnosDBComparatorHelper; -import sqlancer.cnosdb.CnosDBExpectedError; -import sqlancer.cnosdb.CnosDBGlobalState; -import sqlancer.cnosdb.CnosDBVisitor; - -public class CnosDBTLPWhereOracle extends CnosDBTLPBase { - - public CnosDBTLPWhereOracle(CnosDBGlobalState state) { - super(state); - } - - @Override - public void check() throws Exception { - super.check(); - whereCheck(); - } - - protected void whereCheck() throws Exception { - if (Randomly.getBooleanWithRatherLowProbability()) { - select.setOrderByClauses(gen.generateOrderBy()); - } - String originalQueryString = CnosDBVisitor.asString(select); - List resultSet = CnosDBComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, - CnosDBExpectedError.expectedErrors(), state); - - select.setOrderByClauses(Collections.emptyList()); - select.setWhereClause(predicate); - String firstQueryString = CnosDBVisitor.asString(select); - select.setWhereClause(negatedPredicate); - String secondQueryString = CnosDBVisitor.asString(select); - select.setWhereClause(isNullPredicate); - String thirdQueryString = CnosDBVisitor.asString(select); - List combinedString = new ArrayList<>(); - List secondResultSet = CnosDBComparatorHelper.getCombinedResultSet(firstQueryString, secondQueryString, - thirdQueryString, combinedString, Randomly.getBoolean(), state, CnosDBExpectedError.expectedErrors()); - CnosDBComparatorHelper.assumeResultSetsAreEqual(resultSet, secondResultSet, originalQueryString, combinedString, - state); - } -} diff --git a/src/sqlancer/cnosdb/query/CnosDBOtherQuery.java b/src/sqlancer/cnosdb/query/CnosDBOtherQuery.java deleted file mode 100644 index f0a37056c..000000000 --- a/src/sqlancer/cnosdb/query/CnosDBOtherQuery.java +++ /dev/null @@ -1,32 +0,0 @@ -package sqlancer.cnosdb.query; - -import sqlancer.GlobalState; -import sqlancer.IgnoreMeException; -import sqlancer.cnosdb.client.CnosDBConnection; -import sqlancer.common.query.ExpectedErrors; - -public class CnosDBOtherQuery extends CnosDBQueryAdapter { - private static final long serialVersionUID = 1L; - - public CnosDBOtherQuery(String query, ExpectedErrors errors) { - super(query, errors); - } - - @Override - public boolean couldAffectSchema() { - return true; - } - - @Override - public > boolean execute(G globalState, String... fills) - throws Exception { - try { - globalState.getConnection().getClient().execute(query); - } catch (Exception e) { - if (this.errors.errorIsExpected(e.getMessage())) { - throw new IgnoreMeException(); - } - } - return true; - } -} diff --git a/src/sqlancer/cnosdb/query/CnosDBQueryAdapter.java b/src/sqlancer/cnosdb/query/CnosDBQueryAdapter.java deleted file mode 100644 index 115f96ffc..000000000 --- a/src/sqlancer/cnosdb/query/CnosDBQueryAdapter.java +++ /dev/null @@ -1,42 +0,0 @@ -package sqlancer.cnosdb.query; - -import sqlancer.cnosdb.client.CnosDBConnection; -import sqlancer.common.query.ExpectedErrors; -import sqlancer.common.query.Query; - -public abstract class CnosDBQueryAdapter extends Query { - private static final long serialVersionUID = 1L; - - String query; - ExpectedErrors errors; - - public CnosDBQueryAdapter(String query, ExpectedErrors errors) { - this.query = query; - this.errors = errors; - } - - @Override - public String getLogString() { - return query; - } - - @Override - public String getQueryString() { - return query; - } - - @Override - public String getUnterminatedQueryString() { - return null; - } - - @Override - public boolean couldAffectSchema() { - return false; - } - - @Override - public ExpectedErrors getExpectedErrors() { - return errors; - } -} diff --git a/src/sqlancer/cnosdb/query/CnosDBQueryProvider.java b/src/sqlancer/cnosdb/query/CnosDBQueryProvider.java deleted file mode 100644 index dee38abf4..000000000 --- a/src/sqlancer/cnosdb/query/CnosDBQueryProvider.java +++ /dev/null @@ -1,6 +0,0 @@ -package sqlancer.cnosdb.query; - -@FunctionalInterface -public interface CnosDBQueryProvider { - CnosDBOtherQuery getQuery(S globalState) throws Exception; -} diff --git a/src/sqlancer/cnosdb/query/CnosDBSelectQuery.java b/src/sqlancer/cnosdb/query/CnosDBSelectQuery.java deleted file mode 100644 index 1c9228182..000000000 --- a/src/sqlancer/cnosdb/query/CnosDBSelectQuery.java +++ /dev/null @@ -1,39 +0,0 @@ -package sqlancer.cnosdb.query; - -import sqlancer.GlobalState; -import sqlancer.cnosdb.client.CnosDBConnection; -import sqlancer.cnosdb.client.CnosDBResultSet; -import sqlancer.common.query.ExpectedErrors; -import sqlancer.common.query.SQLancerResultSet; - -public class CnosDBSelectQuery extends CnosDBQueryAdapter { - private static final long serialVersionUID = 1L; - CnosDBResultSet resultSet; - - public CnosDBSelectQuery(String query, ExpectedErrors errors) { - super(query, errors); - } - - @Override - public boolean couldAffectSchema() { - return false; - } - - @Override - public > boolean execute(G globalState, String... fills) - throws Exception { - globalState.getConnection().getClient().execute(query); - return false; - } - - @Override - public > SQLancerResultSet executeAndGet(G globalState, - String... fills) throws Exception { - resultSet = globalState.getConnection().getClient().executeQuery(query); - return null; - } - - public CnosDBResultSet getResultSet() { - return resultSet; - } -} diff --git a/src/sqlancer/cockroachdb/CockroachDBSchema.java b/src/sqlancer/cockroachdb/CockroachDBSchema.java index 708afef92..cbde577cb 100644 --- a/src/sqlancer/cockroachdb/CockroachDBSchema.java +++ b/src/sqlancer/cockroachdb/CockroachDBSchema.java @@ -182,6 +182,7 @@ public CockroachDBColumn(String name, CockroachDBCompositeDataType columnType, b this.isNullable = isNullable; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } @@ -292,7 +293,7 @@ public static CockroachDBSchema fromConnection(SQLConnection con, String databas for (String tableName : tableNames) { List databaseColumns = getTableColumns(con, tableName); List indexes = getIndexes(con, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); CockroachDBTable t = new CockroachDBTable(tableName, databaseColumns, indexes, isView); for (CockroachDBColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/cockroachdb/gen/CockroachDBDeleteGenerator.java b/src/sqlancer/cockroachdb/gen/CockroachDBDeleteGenerator.java index 23fbae389..dd1e94aa9 100644 --- a/src/sqlancer/cockroachdb/gen/CockroachDBDeleteGenerator.java +++ b/src/sqlancer/cockroachdb/gen/CockroachDBDeleteGenerator.java @@ -6,31 +6,34 @@ import sqlancer.cockroachdb.CockroachDBSchema.CockroachDBDataType; import sqlancer.cockroachdb.CockroachDBSchema.CockroachDBTable; import sqlancer.cockroachdb.CockroachDBVisitor; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; -public final class CockroachDBDeleteGenerator { +public final class CockroachDBDeleteGenerator extends AbstractDeleteGenerator { - private CockroachDBDeleteGenerator() { + private final CockroachDBGlobalState globalState; + + private CockroachDBDeleteGenerator(CockroachDBGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter delete(CockroachDBGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); + return new CockroachDBDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { CockroachDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - sb.append("DELETE FROM "); - sb.append(table.getName()); + appendDeleteFromTable(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); CockroachDBErrors.addExpressionErrors(errors); - sb.append(CockroachDBVisitor.asString(new CockroachDBExpressionGenerator(globalState) + appendWhereClause(CockroachDBVisitor.asString(new CockroachDBExpressionGenerator(globalState) .setColumns(table.getColumns()).generateExpression(CockroachDBDataType.BOOL.get()))); } else { errors.add("rejected: DELETE without WHERE clause (sql_safe_updates = true)"); } errors.add("foreign key violation"); CockroachDBErrors.addTransactionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/cockroachdb/gen/CockroachDBIndexGenerator.java b/src/sqlancer/cockroachdb/gen/CockroachDBIndexGenerator.java index 4db3588ac..da6d3c609 100644 --- a/src/sqlancer/cockroachdb/gen/CockroachDBIndexGenerator.java +++ b/src/sqlancer/cockroachdb/gen/CockroachDBIndexGenerator.java @@ -21,7 +21,7 @@ public static SQLQueryAdapter create(CockroachDBGlobalState s) { if (s.getSchema().getIndexCount() >= s.getDbmsSpecificOptions().maxNumIndexes) { throw new IgnoreMeException(); } - return new CockroachDBIndexGenerator(s).getQuery(); + return new CockroachDBIndexGenerator(s).getStatement(); } @Override diff --git a/src/sqlancer/cockroachdb/gen/CockroachDBTableGenerator.java b/src/sqlancer/cockroachdb/gen/CockroachDBTableGenerator.java index c8bfa3c6a..8678aff4d 100644 --- a/src/sqlancer/cockroachdb/gen/CockroachDBTableGenerator.java +++ b/src/sqlancer/cockroachdb/gen/CockroachDBTableGenerator.java @@ -32,7 +32,7 @@ public static SQLQueryAdapter generate(CockroachDBGlobalState globalState) { if (globalState.getSchema().getDatabaseTables().size() > globalState.getDbmsSpecificOptions().maxNumTables) { throw new IgnoreMeException(); } - return new CockroachDBTableGenerator(globalState).getQuery(); + return new CockroachDBTableGenerator(globalState).getStatement(); } @Override diff --git a/src/sqlancer/cockroachdb/gen/CockroachDBUpdateGenerator.java b/src/sqlancer/cockroachdb/gen/CockroachDBUpdateGenerator.java index 8dcd605d7..06cc0f8ee 100644 --- a/src/sqlancer/cockroachdb/gen/CockroachDBUpdateGenerator.java +++ b/src/sqlancer/cockroachdb/gen/CockroachDBUpdateGenerator.java @@ -22,10 +22,11 @@ private CockroachDBUpdateGenerator(CockroachDBGlobalState globalState) { } public static SQLQueryAdapter gen(CockroachDBGlobalState globalState) { - return new CockroachDBUpdateGenerator(globalState).generate(); + return new CockroachDBUpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { CockroachDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new CockroachDBExpressionGenerator(globalState).setColumns(columns); @@ -39,8 +40,7 @@ private SQLQueryAdapter generate() { sb.append(" SET "); updateColumns(columns); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(CockroachDBVisitor.asString(gen.generateExpression(CockroachDBDataType.BOOL.get()))); + appendWhereClause(CockroachDBVisitor.asString(gen.generateExpression(CockroachDBDataType.BOOL.get()))); } errors.add("violates unique constraint"); errors.add("violates not-null constraint"); @@ -51,7 +51,6 @@ private SQLQueryAdapter generate() { errors.add("cannot write directly to computed column"); CockroachDBErrors.addExpressionErrors(errors); CockroachDBErrors.addTransactionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/common/gen/AbstractDeleteGenerator.java b/src/sqlancer/common/gen/AbstractDeleteGenerator.java new file mode 100644 index 000000000..f3b5a1955 --- /dev/null +++ b/src/sqlancer/common/gen/AbstractDeleteGenerator.java @@ -0,0 +1,58 @@ +package sqlancer.common.gen; + +public abstract class AbstractDeleteGenerator extends AbstractGenerator { + + protected AbstractDeleteGenerator() { + } + + /** + * Appends {@code DELETE FROM }. + * + * @param tableName + * the name of the table to delete from. + */ + protected void appendDeleteFromTable(String tableName) { + appendDeleteFromTable(tableName, false); + } + + /** + * Appends {@code DELETE FROM [ONLY ]}. + * + * @param tableName + * the name of the table to delete from. + * @param only + * whether to emit the {@code ONLY} keyword (used by some databases to restrict deletion to the named + * table rather than its inheritance descendants). + */ + protected void appendDeleteFromTable(String tableName, boolean only) { + sb.append("DELETE FROM "); + if (only) { + sb.append("ONLY "); + } + sb.append(tableName); + } + + /** + * Appends {@code LIMIT } (with a leading space). + * + * @param value + * the LIMIT value, e.g. an integer literal or already-rendered expression. Converted via + * {@link StringBuilder#append(Object)}. + */ + protected void appendLimitClause(Object value) { + sb.append(" LIMIT "); + sb.append(value); + } + + /** + * Appends {@code RETURNING } (with a leading space). + * + * @param expression + * the rendered RETURNING expression. + */ + protected void appendReturningClause(String expression) { + sb.append(" RETURNING "); + sb.append(expression); + } + +} diff --git a/src/sqlancer/common/gen/AbstractGenerator.java b/src/sqlancer/common/gen/AbstractGenerator.java index c478610c4..dbdf100d5 100644 --- a/src/sqlancer/common/gen/AbstractGenerator.java +++ b/src/sqlancer/common/gen/AbstractGenerator.java @@ -8,12 +8,26 @@ public abstract class AbstractGenerator { protected final ExpectedErrors errors = new ExpectedErrors(); protected final StringBuilder sb = new StringBuilder(); protected boolean canAffectSchema; + protected boolean canonicalizeString = true; - public SQLQueryAdapter getQuery() { + public SQLQueryAdapter getStatement() { buildStatement(); - return new SQLQueryAdapter(sb.toString(), errors, canAffectSchema); + return new SQLQueryAdapter(sb.toString(), errors, canAffectSchema, canonicalizeString); } public abstract void buildStatement(); + /** + * Appends {@code WHERE } (with a leading space). Subclasses are responsible for deciding whether to + * include the WHERE clause, typically based on a randomized boolean. Used by DELETE, UPDATE, partial-INDEX, and + * INSERT...ON CONFLICT generators. + * + * @param condition + * the rendered WHERE condition. + */ + protected void appendWhereClause(String condition) { + sb.append(" WHERE "); + sb.append(condition); + } + } diff --git a/src/sqlancer/common/gen/AbstractIndexGenerator.java b/src/sqlancer/common/gen/AbstractIndexGenerator.java new file mode 100644 index 000000000..bfa62a36c --- /dev/null +++ b/src/sqlancer/common/gen/AbstractIndexGenerator.java @@ -0,0 +1,33 @@ +package sqlancer.common.gen; + +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.common.schema.AbstractTableColumn; + +public abstract class AbstractIndexGenerator> extends AbstractGenerator { + + protected void appendCreateIndex(boolean unique) { + sb.append("CREATE "); + if (unique) { + sb.append("UNIQUE "); + } + sb.append("INDEX "); + } + + protected void appendIndexColumnList(List columns, boolean allowOrdering) { + sb.append("("); + for (int i = 0; i < columns.size(); i++) { + if (i != 0) { + sb.append(", "); + } + sb.append(columns.get(i).getName()); + if (allowOrdering && Randomly.getBoolean()) { + sb.append(" "); + sb.append(Randomly.fromOptions("ASC", "DESC")); + } + } + sb.append(")"); + } + +} diff --git a/src/sqlancer/common/gen/AbstractInsertGenerator.java b/src/sqlancer/common/gen/AbstractInsertGenerator.java index 1a0b2a997..1a1b36b67 100644 --- a/src/sqlancer/common/gen/AbstractInsertGenerator.java +++ b/src/sqlancer/common/gen/AbstractInsertGenerator.java @@ -1,12 +1,26 @@ package sqlancer.common.gen; import java.util.List; +import java.util.stream.Collectors; import sqlancer.Randomly; +import sqlancer.common.schema.AbstractTableColumn; -public abstract class AbstractInsertGenerator { +public abstract class AbstractInsertGenerator> extends AbstractGenerator { - protected StringBuilder sb = new StringBuilder(); + protected void appendColumnList(List columns) { + sb.append("("); + sb.append(columns.stream().map(AbstractTableColumn::getName).collect(Collectors.joining(", "))); + sb.append(")"); + } + + protected void buildInsertInto(String tableName, List columns) { + sb.append("INSERT INTO "); + sb.append(tableName); + appendColumnList(columns); + sb.append(" VALUES "); + insertColumns(columns); + } protected void insertColumns(List columns) { for (int nrRows = 0; nrRows < Randomly.smallNumber() + 1; nrRows++) { diff --git a/src/sqlancer/common/gen/AbstractTableGenerator.java b/src/sqlancer/common/gen/AbstractTableGenerator.java new file mode 100644 index 000000000..c8d1d9697 --- /dev/null +++ b/src/sqlancer/common/gen/AbstractTableGenerator.java @@ -0,0 +1,77 @@ +package sqlancer.common.gen; + +import java.util.List; + +import sqlancer.common.schema.AbstractTableColumn; + +public abstract class AbstractTableGenerator> extends AbstractGenerator { + + /** + * Appends {@code CREATE TABLE }. + * + * @param tableName + * the name of the table to create. + */ + protected void appendCreateTable(String tableName) { + appendCreateTable(tableName, false); + } + + /** + * Appends {@code CREATE TABLE [IF NOT EXISTS ]}. + * + * @param tableName + * the name of the table to create. + * @param ifNotExists + * whether to emit the {@code IF NOT EXISTS} clause. + */ + protected void appendCreateTable(String tableName, boolean ifNotExists) { + sb.append("CREATE TABLE "); + if (ifNotExists) { + sb.append("IF NOT EXISTS "); + } + sb.append(tableName); + } + + /** + * Appends a parenthesized, comma-separated column definition list, e.g. {@code (c0 INT, c1 TEXT)}. Delegates each + * column's rendering to {@link #appendColumnDefinition(AbstractTableColumn)}. + * + * @param columns + * the columns to render. + */ + protected void appendColumnDefinitions(List columns) { + sb.append("("); + appendColumnDefinitionList(columns); + sb.append(")"); + } + + /** + * Appends a comma-separated column definition list without enclosing parentheses, e.g. {@code c0 INT, c1 TEXT}. + * Useful when subclasses also emit table-level constraints (e.g. {@code PRIMARY KEY (...)}) inside the same parens. + * + * @param columns + * the columns to render. + */ + protected void appendColumnDefinitionList(List columns) { + for (int i = 0; i < columns.size(); i++) { + if (i != 0) { + sb.append(", "); + } + appendColumnDefinition(columns.get(i)); + } + } + + /** + * Appends a single column's definition. Default output is {@code }, e.g. {@code c0 INT}. Override to + * add constraints such as {@code NOT NULL}, {@code DEFAULT ...}, or {@code CHECK (...)}. + * + * @param column + * the column whose definition to render. + */ + protected void appendColumnDefinition(C column) { + sb.append(column.getName()); + sb.append(" "); + sb.append(column.getType()); + } + +} diff --git a/src/sqlancer/common/gen/AbstractUpdateGenerator.java b/src/sqlancer/common/gen/AbstractUpdateGenerator.java index f130c15a5..52b716bff 100644 --- a/src/sqlancer/common/gen/AbstractUpdateGenerator.java +++ b/src/sqlancer/common/gen/AbstractUpdateGenerator.java @@ -2,13 +2,9 @@ import java.util.List; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.schema.AbstractTableColumn; -public abstract class AbstractUpdateGenerator> { - - protected final ExpectedErrors errors = new ExpectedErrors(); - protected StringBuilder sb = new StringBuilder(); +public abstract class AbstractUpdateGenerator> extends AbstractGenerator { protected void updateColumns(List columns) { for (int nrColumn = 0; nrColumn < columns.size(); nrColumn++) { diff --git a/src/sqlancer/common/gen/EETDMLGenerator.java b/src/sqlancer/common/gen/EETDMLGenerator.java new file mode 100644 index 000000000..9dbc9f0d5 --- /dev/null +++ b/src/sqlancer/common/gen/EETDMLGenerator.java @@ -0,0 +1,340 @@ +package sqlancer.common.gen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import sqlancer.common.ast.newast.Expression; +import sqlancer.common.oracle.EETTransformer; +import sqlancer.common.schema.AbstractTable; +import sqlancer.common.schema.AbstractTableColumn; +import sqlancer.common.schema.AbstractTables; + +/** + * Generator interface used by {@link sqlancer.common.oracle.EETDMLOracle}, the DML counterpart of {@link EETGenerator}. + * It supplies methods which generate the transformable expressions, create the DBMS-specific {@link EETTransformer} + * that rewrites them, and produce the SQL of the statements the oracle uses to observe the database state a statement + * produces (an approach drawn from the DQE oracle). + * + *

+ * Adapted from the DQE oracle, state is observed with an auxiliary column ({@link EETDMLGenerator#ROW_ID_COLUMN}) which + * uniquely identifies each row. The rows are stamped with identifiers once, before both executions of the statement run + * (each in a rolled-back transaction), so both executions observe the same identifiers. The resulting state is compared + * as a full post-image (each surviving row's identifier and content column values), which covers any of the three DML + * statements (DELETE, UPDATE, INSERT). + * + *

+ * Most of these statements are standard SQL, likely common to most DBMSs, so are provided as {@code default} methods. + * + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific table class + * @param + * the DBMS-specific column class + */ +public interface EETDMLGenerator, T extends AbstractTable, C extends AbstractTableColumn> { + + /** Name of the auxiliary column that uniquely identifies each row. */ + String ROW_ID_COLUMN = "rowid"; + + /** + * Restricts this generator to the given tables (a single table, for the DML statement under test) and their + * columns. + * + * @param tables + * the tables (and, implicitly, columns) the generated statement operates on + * + * @return this generator + */ + EETDMLGenerator setTablesAndColumns(AbstractTables tables); + + /** + * Generates a fresh random boolean expression over the current tables' columns, used as the DML statement's WHERE + * predicate. + * + * @return a fresh random boolean expression + */ + E generateBooleanExpression(); + + /** + * Generates a fresh set of {@code column = value} assignments over the current tables' columns, used as an UPDATE + * statement's SET clause. The columns are a random non-empty subset and each value is a fresh random expression; + * both the columns and their assigned expressions are transformed by the oracle. + * + * @return the assignments, as {@code (column, value expression)} pairs (at least one) + */ + List> generateSetAssignments(); + + /** + * Generates a fresh value expression for each content column of the current table, used as an INSERT statement's + * inserted values. The returned expressions are positionally aligned with {@link AbstractTable#getColumns()}, and + * each is transformed by the oracle. + * + * @return one fresh random value expression per content column, in {@link AbstractTable#getColumns()} order + */ + List generateInsertValues(); + + /** + * Creates a DBMS-specific {@link EETTransformer} backed by this generator, used to rewrite the statement's + * expressions into semantically equivalent ones. + * + * @return a DBMS-specific {@link EETTransformer} + */ + EETTransformer createTransformer(); + + // --- DBMS-specific primitives --- + + /** + * Renders an expression to its DBMS-specific SQL string. + * + * @param expr + * the expression to render + * + * @return the SQL text of {@code expr} + */ + String asString(E expr); + + /** + * SQL that assigns every existing row of {@code table} a distinct, stable identifier in the {@link #ROW_ID_COLUMN} + * column. For example, a 36-character UUID string. + * + * @param table + * the table whose rows are stamped + * + * @return the SQL statement + */ + String stampRowIdsStatement(T table); + + /** + * The SQL type of the auxiliary {@link #ROW_ID_COLUMN} column. It must be able to hold the identifiers that + * {@link #stampRowIdsStatement} produces, so it belongs with that statement as the other half of the row-id + * representation. For example, {@code VARCHAR(36)} would fit a 36-character UUID string. + * + * @return the column type + */ + String rowIdColumnType(); + + /** + * A SQL expression, evaluated once per source row of an {@code INSERT ... SELECT}, that derives the inserted row's + * {@link #ROW_ID_COLUMN} value from the source row's identifier. It must be deterministic (so both the original and + * transformed statements assign the same identifiers), unique per source row, and distinct from every existing + * identifier (so an inserted row never collides with the source row it was derived from in the post-image). DBMS- + * specific because it names a suitable derivation function (e.g. a hash of the source identifier). + * + * @return the SQL expression deriving an inserted row's identifier from the source row's {@link #ROW_ID_COLUMN} + */ + String insertedRowIdExpression(); + + // --- Standard-SQL statements (override only where the DBMS's dialect differs) --- + + /** + * SQL that adds the auxiliary {@link #ROW_ID_COLUMN} column to {@code table}, typed as {@link #rowIdColumnType}. + * + * @param table + * the table to add the column to + * + * @return the SQL statement + */ + default String addRowIdColumnStatement(T table) { + return "ALTER TABLE " + table.getName() + " ADD COLUMN " + ROW_ID_COLUMN + " " + rowIdColumnType(); + } + + /** + * SQL that drops the auxiliary {@link #ROW_ID_COLUMN} column from {@code table}. + * + * @param table + * the table to drop the column from + * + * @return the SQL statement + */ + default String dropRowIdColumnStatement(T table) { + return "ALTER TABLE " + table.getName() + " DROP COLUMN " + ROW_ID_COLUMN; + } + + /** + * SQL that reads back the full post-image of {@code table}: the {@link #ROW_ID_COLUMN} identifier and every content + * column of every surviving row, ordered by the (unique) identifier so the two statements' snapshots align + * row-for-row. + * + *

+ * This single value-level snapshot is the comparison surface for all DML statements: a DELETE removes rows from it, + * an UPDATE changes column values in it, an INSERT adds rows to it. Row identity alone (which the identifier + * already captures) would suffice for DELETE, but not for UPDATE, where the two runs could touch the same rows yet + * write different values. + * + * @param table + * the table to snapshot + * + * @return the SQL statement; its result columns are those of {@link #postImageColumns}, in that order + */ + default String selectPostImageStatement(T table) { + return "SELECT " + String.join(", ", postImageColumns(table)) + " FROM " + table.getName() + " ORDER BY " + + ROW_ID_COLUMN; + } + + /** + * The columns a post-image row consists of, in the order {@link #selectPostImageStatement} returns them: the + * {@link #ROW_ID_COLUMN} identifier followed by {@code table}'s content columns. This is the sole definition of the + * post-image layout, so a consumer can find the identifier's position by looking up {@link #ROW_ID_COLUMN} here + * rather than assuming one. + * + * @param table + * the table being snapshot + * + * @return the post-image column names, in order + */ + default List postImageColumns(T table) { + List columns = new ArrayList<>(); + columns.add(ROW_ID_COLUMN); + for (C column : table.getColumns()) { + columns.add(column.getName()); + } + return columns; + } + + /** + * SQL that deletes the rows of {@code table} matching {@code predicate}, optionally limited to the first + * {@code limit} rows (see {@link #orderByLimitClause}). + * + * @param table + * the table to delete from + * @param predicate + * the WHERE predicate; rendered via {@link #asString} + * @param orderByColumns + * the columns to order by before the row-id tiebreaker (may be empty); only used when {@code limit} is + * non-null + * @param limit + * the maximum number of rows to delete, or {@code null} for no limit + * + * @return the SQL statement + */ + default String deleteStatement(T table, E predicate, List orderByColumns, Integer limit) { + return "DELETE FROM " + table.getName() + " WHERE " + asString(predicate) + + orderByLimitClause(orderByColumns, limit); + } + + /** + * SQL that updates the rows of {@code table} matching {@code predicate}, setting each column in {@code assignments} + * to its assigned value expression, optionally limited to the first {@code limit} rows (see + * {@link #orderByLimitClause}). + * + * @param table + * the table to update + * @param assignments + * the {@code (column, value expression)} pairs to assign; each value is rendered via {@link #asString} + * @param predicate + * the WHERE predicate; rendered via {@link #asString} + * @param orderByColumns + * the columns to order by before the row-id tiebreaker (may be empty); only used when {@code limit} is + * non-null + * @param limit + * the maximum number of rows to update, or {@code null} for no limit + * + * @return the SQL statement + */ + default String updateStatement(T table, List> assignments, E predicate, List orderByColumns, + Integer limit) { + List setClauses = new ArrayList<>(); + for (Map.Entry assignment : assignments) { + setClauses.add(assignment.getKey().getName() + " = " + asString(assignment.getValue())); + } + return "UPDATE " + table.getName() + " SET " + String.join(", ", setClauses) + " WHERE " + asString(predicate) + + orderByLimitClause(orderByColumns, limit); + } + + /** + * SQL that inserts a new row into {@code table} for each source row (optionally filtered by {@code predicate}), + * setting each content column to its corresponding value in {@code values}, optionally limited to the first + * {@code limit} source rows (see {@link #orderByLimitClause}). + * + *

+ * The {@code INSERT ... SELECT} form is used rather than {@code INSERT ... VALUES} because it reuses the source-row + * model already shared by {@link #deleteStatement} and {@link #updateStatement}, and because it offers two kinds of + * transformable expression in one statement (the inserted values and the WHERE predicate) rather than the values + * alone. Each inserted row's {@link #ROW_ID_COLUMN} is derived from its source row via + * {@link #insertedRowIdExpression()}, giving it a deterministic identifier that is unique and distinct from every + * existing one, so the two statements' post-images align (and inserted rows never collide with their source rows). + * + * @param table + * the table to insert into + * @param values + * one value expression per content column, positionally aligned with {@link AbstractTable#getColumns()}; + * each is rendered via {@link #asString} + * @param predicate + * the WHERE predicate filtering the source rows, or {@code null} to insert from every source row; + * rendered via {@link #asString} + * @param orderByColumns + * the columns to order the source rows by before the row-id tiebreaker (may be empty); only used when + * {@code limit} is non-null + * @param limit + * the maximum number of source rows to insert from, or {@code null} for no limit + * + * @return the SQL statement + */ + default String insertStatement(T table, List values, E predicate, List orderByColumns, Integer limit) { + List columnNames = new ArrayList<>(); + columnNames.add(ROW_ID_COLUMN); + List selectItems = new ArrayList<>(); + selectItems.add(insertedRowIdExpression()); + List columns = table.getColumns(); + for (int i = 0; i < columns.size(); i++) { + columnNames.add(columns.get(i).getName()); + selectItems.add(asString(values.get(i))); + } + String statement = "INSERT INTO " + table.getName() + " (" + String.join(", ", columnNames) + ") SELECT " + + String.join(", ", selectItems) + " FROM " + table.getName(); + if (predicate != null) { + statement += " WHERE " + asString(predicate); + } + return statement + orderByLimitClause(orderByColumns, limit); + } + + /** + * Renders the trailing {@code ORDER BY ... LIMIT n} clause shared by {@link #deleteStatement}, + * {@link #updateStatement} and {@link #insertStatement}, or the empty string when {@code limit} is null. + * + *

+ * The rows are ordered by {@code orderByColumns} followed by {@link #ROW_ID_COLUMN} as a tiebreaker. Because the + * identifiers are unique, this is always a total order (even when the ordering columns tie), so the "first + * {@code limit}" rows are identical for the original and transformed statements. Varying the ordering columns + * exercises more access paths than the row id alone would. The caller must pass the same {@code orderByColumns} and + * {@code limit} to both statements; neither is transformed. + * + * @param orderByColumns + * the columns to order by before the row-id tiebreaker (may be empty) + * @param limit + * the maximum number of rows, or {@code null} for no limit (yielding an empty clause) + * + * @return the {@code ORDER BY ... LIMIT n} clause, or the empty string when {@code limit} is null + */ + default String orderByLimitClause(List orderByColumns, Integer limit) { + if (limit == null) { + return ""; + } + List orderBy = new ArrayList<>(); + for (C column : orderByColumns) { + orderBy.add(column.getName()); + } + orderBy.add(ROW_ID_COLUMN); // unique tiebreaker: guarantees a total order regardless of the columns above + return " ORDER BY " + String.join(", ", orderBy) + " LIMIT " + limit; + } + + /** + * SQL that starts a transaction, so a statement's effect can be observed and then undone. + * + * @return the SQL statement + */ + default String beginTransactionStatement() { + return "BEGIN"; + } + + /** + * SQL that rolls the current transaction back, undoing the statement's effect. + * + * @return the SQL statement + */ + default String rollbackTransactionStatement() { + return "ROLLBACK"; + } +} diff --git a/src/sqlancer/common/gen/EETGenerator.java b/src/sqlancer/common/gen/EETGenerator.java new file mode 100644 index 000000000..a87a635ec --- /dev/null +++ b/src/sqlancer/common/gen/EETGenerator.java @@ -0,0 +1,51 @@ +package sqlancer.common.gen; + +import java.util.List; + +import sqlancer.common.ast.newast.Expression; +import sqlancer.common.ast.newast.Join; +import sqlancer.common.ast.newast.Select; +import sqlancer.common.oracle.EETTransformer; +import sqlancer.common.schema.AbstractTable; +import sqlancer.common.schema.AbstractTableColumn; +import sqlancer.common.schema.AbstractTables; + +/** + * Generator interface used by {@link sqlancer.common.oracle.EETOracle}. In addition to generating a random query (like + * the other oracle generators), an EET generator creates a DBMS-specific {@link EETTransformer} that the oracle uses to + * rewrite expressions into semantically equivalent ones. + * + * @param + * the DBMS-specific SELECT statement class + * @param + * the DBMS-specific JOIN clause class + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific table class + * @param + * the DBMS-specific column class + */ +public interface EETGenerator, J extends Join, E extends Expression, T extends AbstractTable, C extends AbstractTableColumn> { + + EETGenerator setTablesAndColumns(AbstractTables tables); + + S generateSelect(); + + List getRandomJoinClauses(); + + List getTableRefs(); + + List generateFetchColumns(boolean shouldCreateDummy); + + E generateBooleanExpression(); + + /** + * Creates a DBMS-specific {@link EETTransformer} backed by this generator. Called once by + * {@link sqlancer.common.oracle.EETOracle} during construction; the oracle owns the returned transformer for the + * lifetime of the test run. + * + * @return a DBMS-specific {@link EETTransformer} backed by this generator + */ + EETTransformer createTransformer(); +} diff --git a/src/sqlancer/common/oracle/AbstractComparisonReproducer.java b/src/sqlancer/common/oracle/AbstractComparisonReproducer.java new file mode 100644 index 000000000..582a19c1d --- /dev/null +++ b/src/sqlancer/common/oracle/AbstractComparisonReproducer.java @@ -0,0 +1,107 @@ +package sqlancer.common.oracle; + +import java.sql.SQLException; + +import sqlancer.Reproducer; +import sqlancer.SQLGlobalState; + +/** + * Shared skeleton for the {@link Reproducer}s of oracles that detect a bug by comparing two evaluations of a + * semantically-equivalent pair (e.g. {@link EETOracle}, {@link NoRECOracle}, {@link TLPWhereOracle}). Reduction re-runs + * both sides against the reduced database and reports whether they still disagree. + * + *

+ * The separate case where the original bug was an unexpected DBMS error rather than a mismatch is handled by + * {@link UnexpectedErrorReproducer}, so a subclass here deals only with comparing two sides and never with error + * handling. + * + *

+ * This class owns the compare-and-report control flow and the {@link #getBugInformation()} header. Subclasses supply + * how each side is evaluated, how the two are compared, and how the failing queries are rendered in the reduced test + * case. + * + * @param + * the DBMS-specific global state class + * @param + * the type each side evaluates to (e.g. a result set as a list of strings, a row count) + */ +public abstract class AbstractComparisonReproducer, R> implements Reproducer { + + /** + * Evaluates the original side against the reduced database. + * + * @param globalState + * the state whose connection points at the reduced database + * + * @return the original side's value + * + * @throws SQLException + * if a DBMS interaction fails + */ + protected abstract R evaluateOriginal(G globalState) throws SQLException; + + /** + * Evaluates the transformed side against the reduced database. + * + * @param globalState + * the state whose connection points at the reduced database + * + * @return the transformed side's value + * + * @throws SQLException + * if a DBMS interaction fails + */ + protected abstract R evaluateTransformed(G globalState) throws SQLException; + + /** + * Whether the two evaluated sides disagree in the way that constitutes the bug. + * + * @param original + * the original side's value + * @param transformed + * the transformed side's value + * @param globalState + * the state the sides were evaluated against + * + * @return {@code true} if the sides differ (i.e. the bug still triggers) + */ + protected abstract boolean sidesDiffer(R original, R transformed, G globalState); + + @Override + public final boolean bugStillTriggers(G globalState) { + R original; + R transformed; + try { + original = evaluateOriginal(globalState); + transformed = evaluateTransformed(globalState); + } catch (AssertionError | SQLException | RuntimeException e) { + // any failure re-running the two sides means this reduced database no longer shows the mismatch + return false; + } + return sidesDiffer(original, transformed, globalState); + } + + @Override + public final String getBugInformation() { + StringBuilder sb = new StringBuilder(); + sb.append(mismatchHeaderLine()).append(System.lineSeparator()); + appendQueryLines(sb); + return sb.toString(); + } + + /** + * The header line (without trailing line separator) describing the mismatch. For example, "-- On the database set + * up by the statements above, the result sets of the following queries mismatch:". + * + * @return the mismatch header line + */ + protected abstract String mismatchHeaderLine(); + + /** + * Appends the failing queries to {@code sb}, one commented line each, so the reduced test case is self-contained. + * + * @param sb + * the builder to append to + */ + protected abstract void appendQueryLines(StringBuilder sb); +} diff --git a/src/sqlancer/common/oracle/DocumentRemovalOracleBase.java b/src/sqlancer/common/oracle/DocumentRemovalOracleBase.java deleted file mode 100644 index b6c0ee509..000000000 --- a/src/sqlancer/common/oracle/DocumentRemovalOracleBase.java +++ /dev/null @@ -1,29 +0,0 @@ -package sqlancer.common.oracle; - -import sqlancer.GlobalState; -import sqlancer.common.gen.ExpressionGenerator; - -public abstract class DocumentRemovalOracleBase> implements TestOracle { - - protected E predicate; - - protected final S state; - - protected DocumentRemovalOracleBase(S state) { - this.state = state; - } - - protected void initializeDocumentRemovalOracle() { - ExpressionGenerator gen = getGen(); - if (gen == null) { - throw new IllegalStateException(); - } - predicate = gen.generatePredicate(); - if (predicate == null) { - throw new IllegalStateException(); - } - } - - protected abstract ExpressionGenerator getGen(); - -} diff --git a/src/sqlancer/common/oracle/EETDMLOracle.java b/src/sqlancer/common/oracle/EETDMLOracle.java new file mode 100644 index 000000000..77a8b528a --- /dev/null +++ b/src/sqlancer/common/oracle/EETDMLOracle.java @@ -0,0 +1,532 @@ +package sqlancer.common.oracle; + +import java.sql.SQLException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import sqlancer.IgnoreMeException; +import sqlancer.Randomly; +import sqlancer.Reproducer; +import sqlancer.SQLGlobalState; +import sqlancer.common.ast.newast.Expression; +import sqlancer.common.gen.EETDMLGenerator; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.common.query.SQLancerResultSet; +import sqlancer.common.schema.AbstractSchema; +import sqlancer.common.schema.AbstractTable; +import sqlancer.common.schema.AbstractTableColumn; +import sqlancer.common.schema.AbstractTables; + +/** + * EET (Equivalent Expression Transformation) oracle for DML statements, based on "Detecting Logic Bugs in Database + * Engines via Equivalent Expression Transformation" (Jiang & Su, OSDI'24). + * + *

+ * Whereas {@link EETOracle} transforms a SELECT and compares the two result sets, this oracle transforms a DML + * statement and compares the two database states produced. + * + *

+ * Adapted from the DQE oracle, state is observed with an auxiliary column ({@link EETDMLGenerator#ROW_ID_COLUMN}) which + * uniquely identifies each row, and each statement is executed inside a transaction that is rolled back, so the two + * statements can be compared against the same starting state without permanently modifying the database. The state is + * captured as a full post-image: each surviving row's identifier together with its content column values, ordered by + * the identifier. This single value-level surface covers every DML statement — a DELETE removes rows from it, an UPDATE + * changes values in it, an INSERT adds rows to it (row identity alone would suffice for DELETE, but not for UPDATE, + * which also transforms the written values). Because rolling back a statement requires a transactional storage engine, + * the DBMS-specific setup must ensure only such engines are used while this oracle is active. + * + *

+ * DELETE, UPDATE and INSERT are currently supported (one is chosen at random per check). INSERT uses the + * {@code INSERT ... SELECT} form so its transformed value expressions may reference columns; each inserted row is given + * a deterministic identifier derived from its source row so the two runs' post-images align. To support reduction, a + * {@link Reproducer} replays the whole comparison (adding and stamping the row-identifier column, running both + * statements in rolled-back transactions and comparing the post-images) against the reduced database. + * + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific schema class + * @param + * the DBMS-specific table class + * @param + * the DBMS-specific column class + * @param + * the DBMS-specific global state class + */ +public class EETDMLOracle, S extends AbstractSchema, T extends AbstractTable, C extends AbstractTableColumn, G extends SQLGlobalState> + implements TestOracle { + + private final G state; + private EETDMLGenerator gen; + private final EETTransformer transformer; + private final ExpectedErrors errors; + + private static final int MAX_DIFF_ROWS_REPORTED = 10; // max differing post-image rows displayed in report log + private String generatedQueryString; + private Reproducer reproducer; + + // The SQL and metadata to run and observe one DML comparison, captured as strings so a reproducer can replay it + // against a reduced database without the generator or live schema objects. + private static final class ComparisonQueries { + private final String originalStatement; + private final String transformedStatement; + private final String addRowIdColumn; + private final String stampRowIds; + private final String beginTransaction; + private final String rollback; + private final String dropRowIdColumn; + private final String selectPostImage; + private final int columnCount; + + ComparisonQueries(String originalStatement, String transformedStatement, String addRowIdColumn, + String stampRowIds, String beginTransaction, String rollback, String dropRowIdColumn, + String selectPostImage, int columnCount) { + this.originalStatement = originalStatement; + this.transformedStatement = transformedStatement; + this.addRowIdColumn = addRowIdColumn; + this.stampRowIds = stampRowIds; + this.beginTransaction = beginTransaction; + this.rollback = rollback; + this.dropRowIdColumn = dropRowIdColumn; + this.selectPostImage = selectPostImage; + this.columnCount = columnCount; + } + } + + // The post-images the original and transformed statements produced, compared for equality to detect the bug. + private static final class PostImages { + private final List> original; + private final List> transformed; + + PostImages(List> original, List> transformed) { + this.original = original; + this.transformed = transformed; + } + } + + // Reproduces a post-image mismatch against the reduced database. Unlike EETOracle's comparison reproducer this does + // not extend AbstractComparisonReproducer: the two sides are not independent, because the row-id stamping (UUID()) + // must run once so both observe the same rows, so both post-images are computed together. + private final class EETDMLReproducer implements Reproducer { + private final ComparisonQueries queries; + + EETDMLReproducer(ComparisonQueries queries) { + this.queries = queries; + } + + @Override + public boolean bugStillTriggers(G globalState) { + PostImages images; + try { + images = computePostImages(globalState, queries); + } catch (AssertionError | SQLException | RuntimeException e) { + // any failure re-running the comparison means this reduced database no longer shows the mismatch + return false; + } + return !images.original.equals(images.transformed); + } + + @Override + public String getBugInformation() { + StringBuilder sb = new StringBuilder(); + sb.append("-- On the database set up by the statements above, the following statements leave the database" + + " in different states:").append(System.lineSeparator()); + renderStatementLines(sb, queries); + return sb.toString(); + } + } + + // Builds the reproducer for an unexpected DBMS error, which replays the whole comparison and checks the same error + // still fires. + private UnexpectedErrorReproducer errorReproducer(ComparisonQueries queries, String expectedErrorMessage) { + UnexpectedErrorReproducer.Execution execution = globalState -> computePostImages(globalState, queries); + StringBuilder sb = new StringBuilder(); + renderStatementLines(sb, queries); + return new UnexpectedErrorReproducer<>(execution, expectedErrorMessage, sb.toString()); + } + + // Renders the failing statements as commented lines, shared by the mismatch and the unexpected-error reproducers. + private static void renderStatementLines(StringBuilder sb, ComparisonQueries queries) { + sb.append("-- original: ").append(queries.originalStatement).append(';').append(System.lineSeparator()); + sb.append("-- transformed: ").append(queries.transformedStatement).append(';').append(System.lineSeparator()); + } + + public EETDMLOracle(G state, EETDMLGenerator gen, ExpectedErrors expectedErrors) { + if (state == null || gen == null || expectedErrors == null) { + throw new IllegalArgumentException("Null variables used to initialize test oracle."); + } + this.state = state; + this.gen = gen; + this.transformer = gen.createTransformer(); + this.errors = expectedErrors; + } + + @Override + public void check() throws SQLException { + reproducer = null; + List tables = state.getSchema().getDatabaseTables(); + if (tables.isEmpty()) { + throw new IgnoreMeException(); + } + // A DML statement targets a single table, so operate on exactly one; confining the generator to it keeps the + // predicate and value expressions from referencing another table's columns (which would render invalid + // single-table DML). + T table = Randomly.fromList(tables); + gen = gen.setTablesAndColumns(new AbstractTables<>(List.of(table))); + + E predicate = gen.generateBooleanExpression(); + // The WHERE predicate is evaluated in a boolean context. + E transformedPredicate = transformer.transform(predicate, true); + + // Optionally cap the statement with a LIMIT. The limit and its ordering (a random column subset, made a total + // order by the row-id tiebreaker) are decided once and applied identically to both statements, so the capped + // row set is deterministic and equal across the runs while still exercising varied orderings. + Integer limit = null; + List orderByColumns = List.of(); + if (Randomly.getBoolean()) { + limit = (int) Randomly.getNotCachedInteger(0, 10); + orderByColumns = Randomly.subset(table.getColumns()); + } + + // Generators for the different kinds of statement this oracle supports. One is chosen at random per check + List> statementGenerators = List.of(this::generateDeleteStatements, + this::generateUpdateStatements, this::generateInsertStatements); + StatementPair statements = Randomly.fromList(statementGenerators).generate(table, predicate, + transformedPredicate, orderByColumns, limit); + String originalStatement = statements.original; + String transformedStatement = statements.transformed; + generatedQueryString = originalStatement; + + // Capture, as strings, everything needed to run and observe this comparison: the two statements plus the + // auxiliary-column setup, per-run snapshot and teardown. A reproducer replays these against a reduced database, + // where the live generator and schema objects no longer apply. + ComparisonQueries queries = new ComparisonQueries(originalStatement, transformedStatement, + gen.addRowIdColumnStatement(table), gen.stampRowIdsStatement(table), gen.beginTransactionStatement(), + gen.rollbackTransactionStatement(), gen.dropRowIdColumnStatement(table), + gen.selectPostImageStatement(table), gen.postImageColumns(table).size()); + + PostImages images; + try { + images = computePostImages(state, queries); + } catch (AssertionError unexpectedError) { + reproducer = errorReproducer(queries, TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + throw unexpectedError; + } + + reproducer = new EETDMLReproducer(queries); + if (!images.original.equals(images.transformed)) { + throw new AssertionError(mismatchMessage(table, originalStatement, transformedStatement, images.original, + images.transformed)); + } + } + + /** + * Runs the whole comparison against {@code globalState}: adds and stamps the row-identifier column once (so both + * runs observe the same rows), snapshots the post-image each statement produces (each inside a rolled-back + * transaction), and drops the column. Both {@link #check()} and the reproducers call this, the former against the + * live database and the latter against a reduced one. A DBMS error the oracle tolerates aborts with + * {@link IgnoreMeException}; an oracle logic bug or unexpected error surfaces as {@link AssertionError}. + * + * @param globalState + * the state whose connection the comparison runs against + * @param queries + * the statements and auxiliary SQL to run + * + * @return the post-images the original and transformed statements produced + * + * @throws SQLException + * if a DBMS interaction fails + */ + private PostImages computePostImages(G globalState, ComparisonQueries queries) throws SQLException { + // Add the auxiliary column outside the try, then guard everything after it with the finally that drops it: the + // ALTER auto-commits (it is not undone by ROLLBACK), so a failure between adding and dropping would leak the + // column and cause cascading duplicate-column failures + if (!new SQLQueryAdapter(queries.addRowIdColumn, errors, true).execute(globalState)) { + throw new IgnoreMeException(); + } + try { + // Stamp identifiers once, in autocommit mode, before both runs: both then observe the same rows. + if (!new SQLQueryAdapter(queries.stampRowIds, errors).execute(globalState)) { + throw new IgnoreMeException(); + } + List> original = snapshotSide(globalState, queries.originalStatement, queries); + List> transformed = snapshotSide(globalState, queries.transformedStatement, queries); + return new PostImages(original, transformed); + } finally { + new SQLQueryAdapter(queries.dropRowIdColumn, errors, true).execute(globalState); + } + } + + /** + * Generates a DML statement of one kind together with its transformed counterpart. The kinds share this signature + * so the oracle can pick one of them at random per check. + * + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific table class + * @param + * the DBMS-specific column class + */ + @FunctionalInterface + private interface DMLStatementGenerator { + StatementPair generate(T table, E predicate, E transformedPredicate, List orderByColumns, Integer limit); + } + + /** + * A DML statement and its transformed counterpart, which must leave the database in the same state. + */ + private static final class StatementPair { + private final String original; + private final String transformed; + + StatementPair(String original, String transformed) { + this.original = original; + this.transformed = transformed; + } + } + + /** + * Generates an UPDATE and its transformed counterpart. Besides the WHERE predicate, UPDATE also transforms the + * written values: each SET value expression is transformed in a scalar context. + * + * @param table + * the table being modified + * @param predicate + * the WHERE predicate of the original statement + * @param transformedPredicate + * the transformed WHERE predicate, used by the transformed statement + * @param orderByColumns + * the columns ordering the statement, empty if it is not capped by a limit + * @param limit + * the maximum number of rows to modify, or {@code null} for no limit + * + * @return the original statement together with its transformed counterpart + */ + private StatementPair generateUpdateStatements(T table, E predicate, E transformedPredicate, List orderByColumns, + Integer limit) { + List> assignments = gen.generateSetAssignments(); + List> transformedAssignments = new ArrayList<>(); + for (Map.Entry assignment : assignments) { + E transformedValue = transformer.transform(assignment.getValue(), false); + transformedAssignments.add(new AbstractMap.SimpleEntry<>(assignment.getKey(), transformedValue)); + } + return new StatementPair(gen.updateStatement(table, assignments, predicate, orderByColumns, limit), + gen.updateStatement(table, transformedAssignments, transformedPredicate, orderByColumns, limit)); + } + + /** + * Generates a DELETE and its transformed counterpart, which differ only in their WHERE predicate. + * + * @param table + * the table being modified + * @param predicate + * the WHERE predicate of the original statement + * @param transformedPredicate + * the transformed WHERE predicate, used by the transformed statement + * @param orderByColumns + * the columns ordering the statement, empty if it is not capped by a limit + * @param limit + * the maximum number of rows to modify, or {@code null} for no limit + * + * @return the original statement together with its transformed counterpart + */ + private StatementPair generateDeleteStatements(T table, E predicate, E transformedPredicate, List orderByColumns, + Integer limit) { + return new StatementPair(gen.deleteStatement(table, predicate, orderByColumns, limit), + gen.deleteStatement(table, transformedPredicate, orderByColumns, limit)); + } + + /** + * Generates an {@code INSERT ... SELECT} and its transformed counterpart. Besides the WHERE predicate, which + * filters the source rows and is optional here, INSERT also transforms each inserted value in a scalar context. + * + *

+ * The ordering and limit cap the source rows the statement reads, so it inserts one row per source row kept. + * + * @param table + * the table being modified + * @param predicate + * the WHERE predicate of the original statement + * @param transformedPredicate + * the transformed WHERE predicate, used by the transformed statement + * @param orderByColumns + * the columns ordering the source rows, empty if the statement is not capped by a limit + * @param limit + * the maximum number of source rows to insert from, or {@code null} for no limit + * + * @return the original statement together with its transformed counterpart + */ + private StatementPair generateInsertStatements(T table, E predicate, E transformedPredicate, List orderByColumns, + Integer limit) { + List values = gen.generateInsertValues(); + List transformedValues = new ArrayList<>(); + for (E value : values) { + transformedValues.add(transformer.transform(value, false)); + } + boolean withPredicate = Randomly.getBoolean(); + return new StatementPair( + gen.insertStatement(table, values, withPredicate ? predicate : null, orderByColumns, limit), + gen.insertStatement(table, transformedValues, withPredicate ? transformedPredicate : null, + orderByColumns, limit)); + } + + /** + * Executes {@code statement} inside a transaction that is always rolled back, and returns the resulting post-image: + * the surviving rows' identifier and content column values, ordered by identifier (the resulting database state). A + * DBMS error the oracle tolerates aborts with {@link IgnoreMeException}; an oracle logic bug or unexpected error + * surfaces as {@link AssertionError}. + * + * @param globalState + * the state whose connection the statement runs against + * @param statement + * the DML statement to execute + * @param queries + * supplies the transaction control and post-image select SQL and the post-image's column count + * + * @return the post-image, as one string list (identifier followed by content column values) per surviving row + * + * @throws SQLException + * if a DBMS interaction other than running {@code statement} fails; an error from {@code statement} + * itself instead surfaces as {@link IgnoreMeException} or {@link AssertionError} + */ + private List> snapshotSide(G globalState, String statement, ComparisonQueries queries) + throws SQLException { + new SQLQueryAdapter(queries.beginTransaction).execute(globalState); + try { + // execute reports (throws AssertionError for) unexpected errors and returns false for expected ones. + boolean succeeded = new SQLQueryAdapter(statement, errors).execute(globalState); + if (!succeeded) { + // The statement hit an error the oracle tolerates; do not compare states (as EETOracle does for + // SELECT). + throw new IgnoreMeException(); + } + return snapshotPostImage(globalState, queries.selectPostImage, queries.columnCount); + } finally { + new SQLQueryAdapter(queries.rollback).execute(globalState); + } + } + + /** + * Reads the post-image produced by {@code selectStatement} into one string list per row (each column via + * {@code getString}). A DBMS error the oracle tolerates aborts with {@link IgnoreMeException}; an oracle logic bug + * or unexpected error surfaces as {@link AssertionError}. + * + * @param globalState + * the state whose connection the select runs against + * @param selectStatement + * the post-image select to read; its columns are the identifier followed by the content columns + * @param columnCount + * the number of columns to read from each row + * + * @return the read rows, in the select's order + * + * @throws SQLException + * if cleanup fails (errors thrown elsewhere will always be rethrown as {@link IgnoreMeException} or + * {@link AssertionError}) + */ + private List> snapshotPostImage(G globalState, String selectStatement, int columnCount) + throws SQLException { + List> rows = new ArrayList<>(); + SQLQueryAdapter q = new SQLQueryAdapter(selectStatement, errors, true, + globalState.getOptions().canonicalizeSqlString()); + SQLancerResultSet result = null; + try { + result = q.executeAndGet(globalState); + if (result == null) { + throw new IgnoreMeException(); + } + while (result.next()) { + List row = new ArrayList<>(columnCount); + for (int i = 1; i <= columnCount; i++) { + row.add(result.getString(i)); + } + rows.add(row); + } + } catch (Exception e) { + if (e instanceof IgnoreMeException) { + throw e; + } + Throwable current = e; + while (current != null) { + if (current.getMessage() != null && errors.errorIsExpected(current.getMessage())) { + throw new IgnoreMeException(); + } + current = current.getCause(); + } + throw new AssertionError(selectStatement, e); + } finally { + if (result != null && !result.isClosed()) { + result.close(); + } + } + return rows; + } + + private String mismatchMessage(T table, String originalStatement, String transformedStatement, + List> originalImage, List> transformedImage) { + List header = gen.postImageColumns(table); + // Where the identifier sits within a post-image row, per the layout the generator defines + int rowIdIndex = header.indexOf(EETDMLGenerator.ROW_ID_COLUMN); + + Map> originalByRowId = indexByRowId(originalImage, rowIdIndex); + Map> transformedByRowId = indexByRowId(transformedImage, rowIdIndex); + Set allRowIds = new TreeSet<>(); + allRowIds.addAll(originalByRowId.keySet()); + allRowIds.addAll(transformedByRowId.keySet()); + + String nl = System.lineSeparator(); + StringBuilder message = new StringBuilder() + .append("-- The original and transformed statements left the database in different states.").append(nl) + .append("-- original: ").append(originalStatement).append(';').append(nl).append("-- transformed: ") + .append(transformedStatement).append(';').append(nl).append("-- differing post-image rows (") + .append(String.join(", ", header)).append("):").append(nl); + int shown = 0; + for (String rowId : allRowIds) { + List originalRow = originalByRowId.get(rowId); + List transformedRow = transformedByRowId.get(rowId); + if (Objects.equals(originalRow, transformedRow)) { + continue; + } + if (shown == MAX_DIFF_ROWS_REPORTED) { + message.append("-- ... (further differences omitted)").append(nl); + break; + } + message.append("-- original: ").append(renderRow(originalRow)).append(nl); + message.append("-- transformed: ").append(renderRow(transformedRow)).append(nl); + shown++; + } + return message.toString(); + } + + // Indexes a post-image by its row identifier, which each row holds at rowIdIndex + private static Map> indexByRowId(List> image, int rowIdIndex) { + Map> byRowId = new LinkedHashMap<>(); + for (List row : image) { + byRowId.put(row.get(rowIdIndex), row); + } + return byRowId; + } + + // Renders a post-image row for the finding message, or "(row absent)" when the row is missing on that side + private static String renderRow(List row) { + return row == null ? "(row absent)" : row.toString(); + } + + @Override + public String getLastQueryString() { + return generatedQueryString; + } + + @Override + public Reproducer getLastReproducer() { + return reproducer; + } +} diff --git a/src/sqlancer/common/oracle/EETOracle.java b/src/sqlancer/common/oracle/EETOracle.java new file mode 100644 index 000000000..0cd4ae99e --- /dev/null +++ b/src/sqlancer/common/oracle/EETOracle.java @@ -0,0 +1,199 @@ +package sqlancer.common.oracle; + +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +import sqlancer.ComparatorHelper; +import sqlancer.Reproducer; +import sqlancer.SQLGlobalState; +import sqlancer.common.ast.newast.Expression; +import sqlancer.common.ast.newast.Join; +import sqlancer.common.ast.newast.Select; +import sqlancer.common.gen.EETGenerator; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.schema.AbstractSchema; +import sqlancer.common.schema.AbstractTable; +import sqlancer.common.schema.AbstractTableColumn; +import sqlancer.common.schema.AbstractTables; + +/** + * EET (Equivalent Expression Transformation) oracle, based on "Detecting Logic Bugs in Database Engines via Equivalent + * Expression Transformation" (Jiang & Su, OSDI'24). + * + *

+ * The oracle generates a random query and then transforms its expressions (the WHERE predicate and the fetch columns) + * into semantically equivalent ones using {@link EETGenerator#transformExpression}. Because the transformation + * preserves semantics, the original and the transformed query must return the same result set; any discrepancy + * indicates a logic bug in the DBMS. + * + * @param + * the DBMS-specific SELECT statement class + * @param + * the DBMS-specific JOIN clause class + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific schema class + * @param + * the DBMS-specific table class + * @param + * the DBMS-specific column class + * @param + * the DBMS-specific global state class + */ +public class EETOracle, J extends Join, E extends Expression, S extends AbstractSchema, T extends AbstractTable, C extends AbstractTableColumn, G extends SQLGlobalState> + implements TestOracle { + + private final G state; + private EETGenerator gen; + private final EETTransformer transformer; + private final ExpectedErrors errors; + + private Reproducer reproducer; + private String generatedQueryString; + + private final class EETReproducer extends AbstractComparisonReproducer> { + private final String originalQueryString; + private final String transformedQueryString; + + EETReproducer(String originalQueryString, String transformedQueryString) { + this.originalQueryString = originalQueryString; + this.transformedQueryString = transformedQueryString; + } + + @Override + protected List evaluateOriginal(G globalState) throws SQLException { + // Re-execute against the current (reduced) database instead of comparing against a cached result set, + // which would be stale once statements have been removed. + return ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, globalState); + } + + @Override + protected List evaluateTransformed(G globalState) throws SQLException { + return ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, errors, globalState); + } + + @Override + protected boolean sidesDiffer(List original, List transformed, G globalState) { + try { + ComparatorHelper.assumeResultSetsAreEqual(original, transformed, originalQueryString, + List.of(transformedQueryString), globalState); + } catch (AssertionError resultSetMismatch) { + return true; + } + return false; + } + + @Override + protected String mismatchHeaderLine() { + return "-- On the database set up by the statements above, the result sets of the following" + + " queries mismatch:"; + } + + @Override + protected void appendQueryLines(StringBuilder sb) { + renderQueryLines(sb, originalQueryString, transformedQueryString); + } + } + + // Renders the failing queries as commented lines, shared by the mismatch and the unexpected-error reproducers. + // transformedQueryString is null when the error struck the original query before any transformation existed. + private static void renderQueryLines(StringBuilder sb, String originalQueryString, String transformedQueryString) { + sb.append("-- original: ").append(originalQueryString).append(';').append(System.lineSeparator()); + if (transformedQueryString != null) { + sb.append("-- transformed: ").append(transformedQueryString).append(';').append(System.lineSeparator()); + } + } + + // Builds the reproducer for an unexpected DBMS error, which re-runs the query (or both queries) and checks the same + // error still fires. transformedQueryString is null when only the original query ran before the error. + private UnexpectedErrorReproducer errorReproducer(String originalQueryString, String transformedQueryString, + String expectedErrorMessage) { + UnexpectedErrorReproducer.Execution execution = globalState -> { + ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, globalState); + if (transformedQueryString != null) { + ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, errors, globalState); + } + }; + StringBuilder sb = new StringBuilder(); + renderQueryLines(sb, originalQueryString, transformedQueryString); + return new UnexpectedErrorReproducer<>(execution, expectedErrorMessage, sb.toString()); + } + + public EETOracle(G state, EETGenerator gen, ExpectedErrors expectedErrors) { + if (state == null || gen == null || expectedErrors == null) { + throw new IllegalArgumentException("Null variables used to initialize test oracle."); + } + this.state = state; + this.gen = gen; + this.transformer = gen.createTransformer(); + this.errors = expectedErrors; + } + + @Override + public void check() throws SQLException { + reproducer = null; + S schema = state.getSchema(); + AbstractTables targetTables = TestOracleUtils.getRandomTableNonEmptyTables(schema); + gen = gen.setTablesAndColumns(targetTables); + + Z select = gen.generateSelect(); + select.setJoinClauses(gen.getRandomJoinClauses()); + select.setFromList(gen.getTableRefs()); + List fetchColumns = gen.generateFetchColumns(true); + select.setFetchColumns(fetchColumns); + E whereClause = gen.generateBooleanExpression(); + select.setWhereClause(whereClause); + + String originalQueryString = select.asString(); + generatedQueryString = originalQueryString; + List originalResultSet; + try { + originalResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, state); + } catch (AssertionError unexpectedError) { + // an unexpected DBMS error on the original query alone is itself a bug worth reducing; + // there is no transformed query yet, so only the original is replayed + reproducer = errorReproducer(originalQueryString, null, + TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + throw unexpectedError; + } + + // Transform the query's expressions into semantically equivalent ones. Fetch columns are scalar expressions, + // while the WHERE clause is evaluated in a boolean context. + List transformedFetchColumns = fetchColumns.stream().map(c -> transformer.transform(c, false)) + .collect(Collectors.toList()); + select.setFetchColumns(transformedFetchColumns); + select.setWhereClause(transformer.transform(whereClause, true)); + + String transformedQueryString = select.asString(); + List transformedResultSet; + try { + transformedResultSet = ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, errors, + state); + } catch (AssertionError unexpectedError) { + // the semantics-preserving transformation made the query trigger a DBMS error that the + // original did not, which is a bug worth reducing + reproducer = errorReproducer(originalQueryString, transformedQueryString, + TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + throw unexpectedError; + } + + // Set the reproducer before the assertion: assumeResultSetsAreEqual throws when the bug is + // detected, so creating the reproducer afterwards would leave it null and prevent any reduction. + reproducer = new EETReproducer(originalQueryString, transformedQueryString); + + ComparatorHelper.assumeResultSetsAreEqual(originalResultSet, transformedResultSet, originalQueryString, + List.of(transformedQueryString), state); + } + + @Override + public Reproducer getLastReproducer() { + return reproducer; + } + + @Override + public String getLastQueryString() { + return generatedQueryString; + } +} diff --git a/src/sqlancer/common/oracle/EETTransformer.java b/src/sqlancer/common/oracle/EETTransformer.java new file mode 100644 index 000000000..b472b8aff --- /dev/null +++ b/src/sqlancer/common/oracle/EETTransformer.java @@ -0,0 +1,360 @@ +package sqlancer.common.oracle; + +import java.util.ArrayList; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.common.ast.newast.Expression; + +/** + * Abstract base class for EET (Equivalent Expression Transformation) tree-walkers, based on "Detecting Logic Bugs in + * Database Engines via Equivalent Expression Transformation" (Jiang & Su, OSDI'24). + * + *

+ * This class implements the seven transformation rules (Table 2 of the paper) and provides a template-method framework + * for applying them throughout an expression's AST. Subclasses implement {@link #descend} to rebuild DBMS-specific AST + * nodes from their transformed children, the abstract factory methods to construct new nodes, and the type hooks + * ({@link #inferType} and {@link #generateExpressionOfType}) that realize the paper's {@code rand_expr(type(expr))}; + * everything else (the rule logic, context threading, and tree-walking orchestration) is provided here. + * + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific type domain used by {@link #inferType} and {@link #generateExpressionOfType} + */ +public abstract class EETTransformer, T> { + + // true_expr(p) = p OR (NOT p) OR (p IS NULL) -> always TRUE + private E trueExpr() { + E p = generateBooleanExpression(); + return orExpr(orExpr(p, not(p)), isNull(p)); + } + + // false_expr(p) = p AND (NOT p) AND (p IS NOT NULL) -> always FALSE + private E falseExpr() { + E p = generateBooleanExpression(); + return and(and(p, not(p)), isNotNull(p)); + } + + /** + * Implements the paper's {@code rand_expr(type(expr))}: a random expression whose static type matches that of + * {@code expr}. Although the generated expression is never evaluated (it occupies the redundant branch of rules 3 + * and 4), its static type participates in the DBMS's CASE WHEN result-type resolution, so a type mismatch could + * alter the live branch's value or rendering. When the type of {@code expr} cannot be inferred, this falls back to + * {@code expr} itself, which trivially has the correct type (degenerating to rules 5 and 6). + * + * @param expr + * the expression whose static type the generated expression must match + * + * @return a random expression whose static type matches that of {@code expr} + */ + private E randExprOfSameType(E expr) { + T type = inferType(expr); + if (type == null) { + return expr; + } + return generateExpressionOfType(type); + } + + /** + * The first six transformation rules of the EET paper (Table 2). Each rule knows how to apply itself + * ({@link #apply}) and in which contexts it preserves the expression's value ({@link #isApplicable}). Rule No. 7 + * (transform the expression to itself) is not modelled here: it is the fallback applied by {@link #applyRandomRule} + * when no other rule is applicable. + */ + private enum Rule { + // expr => false_expr OR expr + RULE_1 { + @Override + , T> E apply(EETTransformer t, E expr) { + return t.orExpr(t.falseExpr(), expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + // Reduces the expression to a boolean value, so it is value-preserving only in a boolean context. + return booleanContext; + } + }, + // expr => true_expr AND expr + RULE_2 { + @Override + , T> E apply(EETTransformer t, E expr) { + return t.and(t.trueExpr(), expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + // Reduces the expression to a boolean value, so it is value-preserving only in a boolean context. + return booleanContext; + } + }, + // expr => CASE WHEN false_expr THEN rand_expr(type(expr)) ELSE expr END + RULE_3 { + @Override + , T> E apply(EETTransformer t, E expr) { + return t.caseWhen(t.falseExpr(), t.randExprOfSameType(expr), expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + return caseWhenApplicable; + } + }, + // expr => CASE WHEN true_expr THEN expr ELSE rand_expr(type(expr)) END + RULE_4 { + @Override + , T> E apply(EETTransformer t, E expr) { + return t.caseWhen(t.trueExpr(), expr, t.randExprOfSameType(expr)); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + return caseWhenApplicable; + } + }, + // expr => CASE WHEN rand_expr(boolean) THEN copy(expr) ELSE expr END + RULE_5 { + @Override + , T> E apply(EETTransformer t, E expr) { + // deep copy of expr is not needed, as the AST nodes are immutable anyway + return t.caseWhen(t.generateBooleanExpression(), expr, expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + return caseWhenApplicable; + } + }, + // expr => CASE WHEN rand_expr(boolean) THEN expr ELSE copy(expr) END + RULE_6 { + @Override + , T> E apply(EETTransformer t, E expr) { + // deep copy of expr is not needed, as the AST nodes are immutable anyway + return t.caseWhen(t.generateBooleanExpression(), expr, expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + return caseWhenApplicable; + } + }; + + /** + * Applies this rule to {@code expr}, producing a semantically equivalent expression. + * + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific type domain + * @param t + * the transformer providing the DBMS-specific node factories + * @param expr + * the expression to transform + * + * @return a semantically equivalent expression + */ + abstract , T> E apply(EETTransformer t, E expr); + + /** + * Whether this rule preserves {@code expr}'s value in the given context. + * + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value (rules 1 and 2 are only applicable if + * this is the case) + * @param caseWhenApplicable + * whether {@code expr} may be wrapped in a CASE WHEN expression + * + * @return {@code true} if this rule preserves {@code expr}'s value in the given context + */ + abstract boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable); + } + + /** + * Applies a randomly chosen applicable transformation rule to {@code expr}, returning a semantically equivalent + * expression. When no rule is applicable, {@code expr} is returned unchanged (rule No. 7 of the EET paper). + * + * @param expr + * the expression to transform + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value + * + * @return a semantically equivalent expression + */ + protected E applyRandomRule(E expr, boolean booleanContext) { + boolean caseWhenApplicable = isCaseWhenApplicable(expr); + List applicableRules = new ArrayList<>(); + for (Rule rule : Rule.values()) { + if (rule.isApplicable(booleanContext, caseWhenApplicable)) { + applicableRules.add(rule); + } + } + if (applicableRules.isEmpty()) { + return expr; // rule 7 fallback: transform expression to itself + } + return Randomly.fromList(applicableRules).apply(this, expr); + } + + /** + * Transforms {@code expr} into a semantically equivalent expression. A transformation rule is always applied at the + * root, guaranteeing (unless only rule 7 is applicable) that the returned expression differs from the input. + * + * @param expr + * the expression to transform + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value + * + * @return a semantically equivalent expression + */ + public E transform(E expr, boolean booleanContext) { + return transformNode(expr, booleanContext, true); + } + + /** + * Descends into {@code expr}, rebuilds it from transformed children, then optionally applies a rule at this node. + * + * @param expr + * the expression to transform + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value + * @param forceApply + * whether a rule must be applied at this node rather than only with some probability + * + * @return the transformed expression + */ + protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { + E descended = descend(expr, booleanContext); + if (forceApply || Randomly.getBoolean()) { + return applyRandomRule(descended, booleanContext); + } + return descended; + } + + /** + * Rebuilds {@code expr} with its children transformed, threading the correct boolean/scalar context into each + * child. Leaf nodes (columns, constants, table references, ...) should be returned unchanged; any applicable + * transformation will still be applied to them by the calling {@link #transformNode}. + * + * @param expr + * the expression to descend into + * @param booleanContext + * the context in which {@code expr} itself is evaluated (used to determine child contexts) + * + * @return a rebuilt copy of {@code expr} with transformed children, or {@code expr} itself if it is a leaf + */ + protected abstract E descend(E expr, boolean booleanContext); + + /** + * Builds {@code left AND right}. + * + * @param left + * the left operand + * @param right + * the right operand + * + * @return the {@code left AND right} expression + */ + protected abstract E and(E left, E right); + + /** + * Builds {@code left OR right}. + * + * @param left + * the left operand + * @param right + * the right operand + * + * @return the {@code left OR right} expression + */ + protected abstract E orExpr(E left, E right); + + /** + * Builds {@code NOT expr}. + * + * @param expr + * the operand + * + * @return the {@code NOT expr} expression + */ + protected abstract E not(E expr); + + /** + * Builds {@code expr IS NULL}. + * + * @param expr + * the operand + * + * @return the {@code expr IS NULL} expression + */ + protected abstract E isNull(E expr); + + /** + * Builds {@code expr IS NOT NULL}. + * + * @param expr + * the operand + * + * @return the {@code expr IS NOT NULL} expression + */ + protected abstract E isNotNull(E expr); + + /** + * Builds {@code CASE WHEN condition THEN thenExpr ELSE elseExpr END}. + * + * @param condition + * the WHEN condition + * @param thenExpr + * the THEN branch + * @param elseExpr + * the ELSE branch + * + * @return the CASE WHEN expression + */ + protected abstract E caseWhen(E condition, E thenExpr, E elseExpr); + + /** + * Generates a fresh random boolean expression, reusing the variables available to the query generator. + * + * @return a fresh random boolean expression + */ + protected abstract E generateBooleanExpression(); + + /** + * Infers the static type of {@code expr}, or returns {@code null} if it cannot be determined. The type domain + * {@code T} is DBMS-specific and may be coarse: it only needs to be precise enough that replacing an expression + * with another of the same {@code T} leaves the DBMS's CASE WHEN result-type resolution unaffected. Returning + * {@code null} is always safe — rules No. 3 and 4 then fall back to reusing {@code expr} itself as the dead branch. + * Inference should therefore be conservative: prefer {@code null} over a type whose CASE WHEN behaviour is + * uncertain. + * + * @param expr + * the expression whose static type is to be inferred + * + * @return the inferred static type of {@code expr}, or {@code null} if it cannot be determined + */ + protected abstract T inferType(E expr); + + /** + * Generates a fresh random expression of static type {@code type}, reusing the variables available to the query + * generator. DBMSs with a typed expression generator can delegate to it directly; DBMSs with an untyped generator + * can instead wrap an arbitrary random expression in a CAST to {@code type} (which requires every value of + * {@code T} to be a valid CAST target). + * + * @param type + * the static type the generated expression must have + * + * @return a fresh random expression of static type {@code type} + */ + protected abstract E generateExpressionOfType(T type); + + /** + * Whether {@code expr} may be wrapped in a CASE WHEN expression. Some expressions (e.g. table references) are not + * CASE-WHEN applicable and must be transformed to themselves (rule No. 7 of the EET paper). + * + * @param expr + * the expression to test + * + * @return {@code true} if {@code expr} may be wrapped in a CASE WHEN expression + */ + protected abstract boolean isCaseWhenApplicable(E expr); +} diff --git a/src/sqlancer/common/oracle/NoRECBase.java b/src/sqlancer/common/oracle/NoRECBase.java deleted file mode 100644 index 2ac0dbb43..000000000 --- a/src/sqlancer/common/oracle/NoRECBase.java +++ /dev/null @@ -1,26 +0,0 @@ -package sqlancer.common.oracle; - -import sqlancer.Main.StateLogger; -import sqlancer.MainOptions; -import sqlancer.SQLConnection; -import sqlancer.SQLGlobalState; -import sqlancer.common.query.ExpectedErrors; - -public abstract class NoRECBase> implements TestOracle { - - protected final S state; - protected final ExpectedErrors errors = new ExpectedErrors(); - protected final StateLogger logger; - protected final MainOptions options; - protected final SQLConnection con; - protected String optimizedQueryString; - protected String unoptimizedQueryString; - - protected NoRECBase(S state) { - this.state = state; - this.con = state.getConnection(); - this.logger = state.getLogger(); - this.options = state.getOptions(); - } - -} diff --git a/src/sqlancer/common/oracle/NoRECOracle.java b/src/sqlancer/common/oracle/NoRECOracle.java index caf3dff87..ba48e80bb 100644 --- a/src/sqlancer/common/oracle/NoRECOracle.java +++ b/src/sqlancer/common/oracle/NoRECOracle.java @@ -1,7 +1,6 @@ package sqlancer.common.oracle; import java.sql.SQLException; -import java.util.Objects; import java.util.function.Function; import sqlancer.IgnoreMeException; @@ -31,19 +30,68 @@ public class NoRECOracle, J extends Join, private Reproducer reproducer; private String lastQueryString; - private static class NoRECReproducer> implements Reproducer { + private static class NoRECReproducer> + extends AbstractComparisonReproducer { private final Function optimizedQuery; private final Function unoptimizedQuery; + private final String optimizedQueryString; + private final String unoptimizedQueryString; - NoRECReproducer(Function optimizedQuery, Function unoptimizedQuery) { + NoRECReproducer(Function optimizedQuery, Function unoptimizedQuery, + String optimizedQueryString, String unoptimizedQueryString) { this.optimizedQuery = optimizedQuery; this.unoptimizedQuery = unoptimizedQuery; + this.optimizedQueryString = optimizedQueryString; + this.unoptimizedQueryString = unoptimizedQueryString; } @Override - public boolean bugStillTriggers(G globalState) { - return !Objects.equals(optimizedQuery.apply(globalState), unoptimizedQuery.apply(globalState)); + protected Integer evaluateOriginal(G globalState) { + return optimizedQuery.apply(globalState); } + + @Override + protected Integer evaluateTransformed(G globalState) { + return unoptimizedQuery.apply(globalState); + } + + @Override + protected boolean sidesDiffer(Integer optimizedCount, Integer unoptimizedCount, G globalState) { + if (optimizedCount == -1 || unoptimizedCount == -1) { + return false; + } + return optimizedCount.intValue() != unoptimizedCount.intValue(); + } + + @Override + protected String mismatchHeaderLine() { + return "-- On the database set up by the statements above, the row counts of the following" + + " queries mismatch:"; + } + + @Override + protected void appendQueryLines(StringBuilder sb) { + renderQueryLines(sb, optimizedQueryString, unoptimizedQueryString); + } + } + + // Renders the failing queries as commented lines, shared by the mismatch and the unexpected-error reproducers. + private static void renderQueryLines(StringBuilder sb, String optimizedQueryString, String unoptimizedQueryString) { + sb.append("-- optimized: ").append(optimizedQueryString).append(';').append(System.lineSeparator()); + sb.append("-- unoptimized: ").append(unoptimizedQueryString).append(';').append(System.lineSeparator()); + } + + // Builds the reproducer for an unexpected DBMS error, which re-runs both queries and checks the same error fires. + private static > UnexpectedErrorReproducer errorReproducer( + Function optimizedQuery, Function unoptimizedQuery, String optimizedQueryString, + String unoptimizedQueryString, String expectedErrorMessage) { + UnexpectedErrorReproducer.Execution execution = globalState -> { + optimizedQuery.apply(globalState); + unoptimizedQuery.apply(globalState); + }; + StringBuilder sb = new StringBuilder(); + renderQueryLines(sb, optimizedQueryString, unoptimizedQueryString); + return new UnexpectedErrorReproducer<>(execution, expectedErrorMessage, sb.toString()); } public NoRECOracle(G state, NoRECGenerator gen, ExpectedErrors expectedErrors) { @@ -82,21 +130,28 @@ public void check() throws SQLException { state.getLogger().writeCurrent(unoptimizedQueryString); } - int optimizedCount = shouldUseAggregate ? extractCounts(optimizedQueryString, errors, state) - : countRows(optimizedQueryString, errors, state); - int unoptimizedCount = extractCounts(unoptimizedQueryString, errors, state); + Function optimizedQuery = state -> shouldUseAggregate + ? extractCounts(optimizedQueryString, errors, state) : countRows(optimizedQueryString, errors, state); + Function unoptimizedQuery = state -> extractCounts(unoptimizedQueryString, errors, state); + + int optimizedCount; + int unoptimizedCount; + try { + optimizedCount = optimizedQuery.apply(state); + unoptimizedCount = unoptimizedQuery.apply(state); + } catch (AssertionError unexpectedError) { + reproducer = errorReproducer(optimizedQuery, unoptimizedQuery, optimizedQueryString, unoptimizedQueryString, + TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + throw unexpectedError; + } if (optimizedCount == -1 || unoptimizedCount == -1) { throw new IgnoreMeException(); } if (unoptimizedCount != optimizedCount) { - Function optimizedQuery = state -> shouldUseAggregate - ? extractCounts(optimizedQueryString, errors, state) - : countRows(optimizedQueryString, errors, state); - - Function unoptimizedQuery = state -> extractCounts(unoptimizedQueryString, errors, state); - reproducer = new NoRECReproducer<>(optimizedQuery, unoptimizedQuery); + reproducer = new NoRECReproducer<>(optimizedQuery, unoptimizedQuery, optimizedQueryString, + unoptimizedQueryString); String queryFormatString = "-- %s;\n-- count: %d"; String firstQueryStringWithCount = String.format(queryFormatString, optimizedQueryString, optimizedCount); diff --git a/src/sqlancer/common/oracle/TLPWhereOracle.java b/src/sqlancer/common/oracle/TLPWhereOracle.java index 14834a62f..5e1d2861b 100644 --- a/src/sqlancer/common/oracle/TLPWhereOracle.java +++ b/src/sqlancer/common/oracle/TLPWhereOracle.java @@ -29,38 +29,103 @@ public class TLPWhereOracle, J extends Join reproducer; private String generatedQueryString; - private class TLPWhereReproducer implements Reproducer { + // A side's result set, together with the human-readable combined-query strings that + // getCombinedResultSet fills in for the transformed side (unused, and null, for the original side) + private static final class TLPResultSet { + final List resultSet; + final List combinedString; + + TLPResultSet(List resultSet, List combinedString) { + this.resultSet = resultSet; + this.combinedString = combinedString; + } + } + + private class TLPWhereReproducer extends AbstractComparisonReproducer { final String firstQueryString; final String secondQueryString; final String thirdQueryString; final String originalQueryString; - final List resultSet; final boolean orderBy; TLPWhereReproducer(String firstQueryString, String secondQueryString, String thirdQueryString, - String originalQueryString, List resultSet, boolean orderBy) { + String originalQueryString, boolean orderBy) { this.firstQueryString = firstQueryString; this.secondQueryString = secondQueryString; this.thirdQueryString = thirdQueryString; this.originalQueryString = originalQueryString; - this.resultSet = resultSet; this.orderBy = orderBy; } @Override - public boolean bugStillTriggers(G globalState) { + protected TLPResultSet evaluateOriginal(G globalState) throws SQLException { + return new TLPResultSet( + ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, globalState), null); + } + + @Override + protected TLPResultSet evaluateTransformed(G globalState) throws SQLException { + List combinedString = new ArrayList<>(); + List secondResultSet = ComparatorHelper.getCombinedResultSet(firstQueryString, secondQueryString, + thirdQueryString, combinedString, !orderBy, globalState, errors); + return new TLPResultSet(secondResultSet, combinedString); + } + + @Override + protected boolean sidesDiffer(TLPResultSet original, TLPResultSet transformed, G globalState) { try { - List combinedString1 = new ArrayList<>(); - List secondResultSet1 = ComparatorHelper.getCombinedResultSet(firstQueryString, - secondQueryString, thirdQueryString, combinedString1, !orderBy, globalState, errors); - ComparatorHelper.assumeResultSetsAreEqual(resultSet, secondResultSet1, originalQueryString, - combinedString1, globalState); - } catch (AssertionError triggeredError) { + ComparatorHelper.assumeResultSetsAreEqual(original.resultSet, transformed.resultSet, + originalQueryString, transformed.combinedString, globalState); + } catch (AssertionError resultSetMismatch) { return true; - } catch (SQLException ignored) { } return false; } + + @Override + protected String mismatchHeaderLine() { + return "-- On the database set up by the statements above, the result sets of the following" + + " queries mismatch:"; + } + + @Override + protected void appendQueryLines(StringBuilder sb) { + renderQueryLines(sb, originalQueryString, firstQueryString, secondQueryString, thirdQueryString, orderBy); + } + } + + // Renders the failing queries as commented lines, shared by the mismatch and the unexpected-error reproducers. The + // partition queries are null when the error struck the original query before they were built. + private static void renderQueryLines(StringBuilder sb, String originalQueryString, String firstQueryString, + String secondQueryString, String thirdQueryString, boolean orderBy) { + sb.append("-- ").append(originalQueryString).append(';').append(System.lineSeparator()); + if (firstQueryString != null) { + if (orderBy) { + sb.append("-- ").append(firstQueryString).append(';').append(System.lineSeparator()); + sb.append("-- ").append(secondQueryString).append(';').append(System.lineSeparator()); + sb.append("-- ").append(thirdQueryString).append(';').append(System.lineSeparator()); + } else { + sb.append("-- ").append(firstQueryString).append(" UNION ALL ").append(secondQueryString) + .append(" UNION ALL ").append(thirdQueryString).append(';').append(System.lineSeparator()); + } + } + } + + // Builds the reproducer for an unexpected DBMS error, which re-runs the query (or the whole-table query plus the + // three partition queries) and checks the same error still fires. The partition queries are null when the error + // struck the original query before they were built. + private UnexpectedErrorReproducer errorReproducer(String originalQueryString, String firstQueryString, + String secondQueryString, String thirdQueryString, boolean orderBy, String expectedErrorMessage) { + UnexpectedErrorReproducer.Execution execution = globalState -> { + ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, globalState); + if (firstQueryString != null) { + ComparatorHelper.getCombinedResultSet(firstQueryString, secondQueryString, thirdQueryString, + new ArrayList<>(), !orderBy, globalState, errors); + } + }; + StringBuilder sb = new StringBuilder(); + renderQueryLines(sb, originalQueryString, firstQueryString, secondQueryString, thirdQueryString, orderBy); + return new UnexpectedErrorReproducer<>(execution, expectedErrorMessage, sb.toString()); } public TLPWhereOracle(G state, TLPWhereGenerator gen, ExpectedErrors expectedErrors) { @@ -89,8 +154,14 @@ public void check() throws SQLException { String originalQueryString = select.asString(); generatedQueryString = originalQueryString; - List firstResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, - state); + List firstResultSet; + try { + firstResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, state); + } catch (AssertionError unexpectedError) { + reproducer = errorReproducer(originalQueryString, null, null, null, false, + TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + throw unexpectedError; + } boolean orderBy = Randomly.getBooleanWithSmallProbability(); if (orderBy) { @@ -107,14 +178,20 @@ public void check() throws SQLException { String thirdQueryString = select.asString(); List combinedString = new ArrayList<>(); - List secondResultSet = ComparatorHelper.getCombinedResultSet(firstQueryString, secondQueryString, - thirdQueryString, combinedString, !orderBy, state, errors); + List secondResultSet; + try { + secondResultSet = ComparatorHelper.getCombinedResultSet(firstQueryString, secondQueryString, + thirdQueryString, combinedString, !orderBy, state, errors); + } catch (AssertionError unexpectedError) { + reproducer = errorReproducer(originalQueryString, firstQueryString, secondQueryString, thirdQueryString, + orderBy, TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + throw unexpectedError; + } + reproducer = new TLPWhereReproducer(firstQueryString, secondQueryString, thirdQueryString, originalQueryString, + orderBy); ComparatorHelper.assumeResultSetsAreEqual(firstResultSet, secondResultSet, originalQueryString, combinedString, state); - - reproducer = new TLPWhereReproducer(firstQueryString, secondQueryString, thirdQueryString, originalQueryString, - firstResultSet, orderBy); } @Override diff --git a/src/sqlancer/common/oracle/TestOracleUtils.java b/src/sqlancer/common/oracle/TestOracleUtils.java index bab2e26c9..9bef86762 100644 --- a/src/sqlancer/common/oracle/TestOracleUtils.java +++ b/src/sqlancer/common/oracle/TestOracleUtils.java @@ -34,6 +34,28 @@ public static final class PredicateVariants, C extends A return new AbstractTables<>(Randomly.nonEmptySubset(schema.getDatabaseTables())); } + /** + * Extracts the message of the DBMS error that caused an oracle query to fail unexpectedly, from the AssertionError + * that wraps it (see, e.g., ComparatorHelper#getResultSetFirstColumnAsString). Reproducers use it to check that a + * reduced test case still triggers the same error, rather than an unrelated one introduced by the reduction itself. + * + * @param error + * the AssertionError wrapping the DBMS error + * + * @return the message of the innermost cause that has one, or the error's own message + */ + public static String getUnexpectedErrorMessage(AssertionError error) { + String message = error.getMessage(); + Throwable current = error.getCause(); + while (current != null) { + if (current.getMessage() != null) { + message = current.getMessage(); + } + current = current.getCause(); + } + return message; + } + public static , T extends AbstractTable, C extends AbstractTableColumn> PredicateVariants initializeTernaryPredicateVariants( PartitionGenerator gen, E predicate) { if (gen == null) { diff --git a/src/sqlancer/common/oracle/UnexpectedErrorReproducer.java b/src/sqlancer/common/oracle/UnexpectedErrorReproducer.java new file mode 100644 index 000000000..e9390943a --- /dev/null +++ b/src/sqlancer/common/oracle/UnexpectedErrorReproducer.java @@ -0,0 +1,79 @@ +package sqlancer.common.oracle; + +import java.sql.SQLException; + +import sqlancer.Reproducer; +import sqlancer.SQLGlobalState; + +/** + * Reproducer for a bug that is an unexpected DBMS error, rather than a violation of oracle logic. When a statement run + * by the oracle raises an error the oracle did not expect, the bug is that error. Reduction re-runs the statement + * execution against the reduced database and reports whether the same error still fires. The oracle supplies how to + * re-run its execution as an {@link Execution} functional interface. + * + *

+ * For an oracle to use this reproducer on its unexpected errors, they must surface as {@link AssertionError}s, even + * though they likely originated as {@link SQLException}s. This is because + * {@link UnexpectedErrorReproducer#bugStillTriggers} treats a {@link SQLException} as a replay failure that means "bug + * does not trigger" (e.g. dropped connection, removed table). + * + * @param + * the DBMS-specific global state class + */ +public final class UnexpectedErrorReproducer> implements Reproducer { + + private final Execution execution; + private final String expectedErrorMessage; + private final String queryLines; + + @FunctionalInterface + public interface Execution { + /** + * Re-runs the oracle's execution against the reduced database. The bug is treated as still present if the run + * continues to raise an {@link AssertionError} with the same message. + * + * @param globalState + * the state whose connection points at the reduced database + * + * @throws SQLException + * if a DBMS interaction fails for a reason other than the recorded bug (e.g. a connection or setup + * failure during replay), which counts as the bug no longer triggering + */ + void execute(G globalState) throws SQLException; + } + + /** + * @param execution + * re-runs the oracle's execution against the reduced database + * @param expectedErrorMessage + * the message of the error the original bug was. The bug is treated as still present if the + * {@link Execution} continues to raise an {@link AssertionError} with the same message. + * @param queryLines + * the failing queries as commented lines (each ending in a line separator), for the reduced test case + */ + public UnexpectedErrorReproducer(Execution execution, String expectedErrorMessage, String queryLines) { + this.execution = execution; + this.expectedErrorMessage = expectedErrorMessage; + this.queryLines = queryLines; + } + + @Override + public boolean bugStillTriggers(G globalState) { + try { + execution.execute(globalState); + } catch (AssertionError unexpectedError) { + // the same error reproduces the bug; a different one is an artifact of the reduction (e.g. a removed table) + return expectedErrorMessage.equals(TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + } catch (SQLException | RuntimeException e) { + return false; + } + // the error no longer fires + return false; + } + + @Override + public String getBugInformation() { + return "-- On the database set up by the statements above, the following queries trigger an unexpected error" + + " with message: " + expectedErrorMessage + System.lineSeparator() + queryLines; + } +} diff --git a/src/sqlancer/common/schema/AbstractTable.java b/src/sqlancer/common/schema/AbstractTable.java index 89d568867..58154681c 100644 --- a/src/sqlancer/common/schema/AbstractTable.java +++ b/src/sqlancer/common/schema/AbstractTable.java @@ -95,6 +95,10 @@ public boolean isView() { return isView; } + public boolean hasPrimaryKey() { + return columns.stream().anyMatch(c -> c.isPrimaryKey()); + } + public String getFreeColumnName() { int i = 0; if (Randomly.getBooleanWithRatherLowProbability()) { diff --git a/src/sqlancer/common/schema/AbstractTableColumn.java b/src/sqlancer/common/schema/AbstractTableColumn.java index e519bea97..a2f5fb1b3 100644 --- a/src/sqlancer/common/schema/AbstractTableColumn.java +++ b/src/sqlancer/common/schema/AbstractTableColumn.java @@ -12,6 +12,10 @@ public AbstractTableColumn(String name, T table, U type) { this.type = type; } + public boolean isPrimaryKey() { + return false; + } + public String getName() { return name; } diff --git a/src/sqlancer/databend/DatabendBugs.java b/src/sqlancer/databend/DatabendBugs.java index dd11512d8..ae2f74a33 100644 --- a/src/sqlancer/databend/DatabendBugs.java +++ b/src/sqlancer/databend/DatabendBugs.java @@ -19,6 +19,7 @@ public final class DatabendBugs { public static boolean bug15569 = true; // https://github.com/datafuselabs/databend/issues/15569 public static boolean bug15570 = true; // https://github.com/datafuselabs/databend/issues/15570 public static boolean bug15572 = true; // https://github.com/datafuselabs/databend/issues/15572 + public static boolean bug19773 = true; // https://github.com/databendlabs/databend/issues/19773 private DatabendBugs() { } diff --git a/src/sqlancer/databend/DatabendErrors.java b/src/sqlancer/databend/DatabendErrors.java index fdd8a3a69..3e056d003 100644 --- a/src/sqlancer/databend/DatabendErrors.java +++ b/src/sqlancer/databend/DatabendErrors.java @@ -47,6 +47,11 @@ public static List getExpressionErrors() { if (DatabendBugs.bug15568) { errors.add("Decimal overflow at line : 723 while evaluating function `to_decimal"); } + if (DatabendBugs.bug19773) { + errors.add("failed to downcast column Decimal128"); + errors.add("Decimal(DecimalSize { precision: 38"); + errors.add("_eager_final_count"); + } /* * TODO column为not null 时,注意default不能为null DROP DATABASE IF EXISTS databend2; CREATE DATABASE databend2; USE diff --git a/src/sqlancer/databend/DatabendSchema.java b/src/sqlancer/databend/DatabendSchema.java index af9a3e0ce..89738a1f3 100644 --- a/src/sqlancer/databend/DatabendSchema.java +++ b/src/sqlancer/databend/DatabendSchema.java @@ -148,6 +148,7 @@ public DatabendColumn(String name, DatabendCompositeDataType columnType, boolean this.isNullable = isNullable; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } @@ -321,7 +322,7 @@ public static DatabendSchema fromConnection(SQLConnection con, String databaseNa List tableNames = getTableNames(con, databaseName); for (String tableName : tableNames) { List databaseColumns = getTableColumns(con, tableName, databaseName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); DatabendTable t = new DatabendTable(tableName, databaseColumns, isView); for (DatabendColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/databend/gen/DatabendDeleteGenerator.java b/src/sqlancer/databend/gen/DatabendDeleteGenerator.java index 256030409..22336fd72 100644 --- a/src/sqlancer/databend/gen/DatabendDeleteGenerator.java +++ b/src/sqlancer/databend/gen/DatabendDeleteGenerator.java @@ -1,29 +1,33 @@ package sqlancer.databend.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.databend.DatabendErrors; import sqlancer.databend.DatabendProvider.DatabendGlobalState; import sqlancer.databend.DatabendSchema.DatabendDataType; import sqlancer.databend.DatabendToStringVisitor; -public final class DatabendDeleteGenerator { +public final class DatabendDeleteGenerator extends AbstractDeleteGenerator { - private DatabendDeleteGenerator() { + private final DatabendGlobalState globalState; + + private DatabendDeleteGenerator(DatabendGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter generate(DatabendGlobalState globalState) { - StringBuilder sb = new StringBuilder("DELETE FROM "); - ExpectedErrors errors = new ExpectedErrors(); - sb.append(globalState.getSchema().getRandomTable(t -> !t.isView()).getName()); + return new DatabendDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { + appendDeleteFromTable(globalState.getSchema().getRandomTable(t -> !t.isView()).getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(DatabendToStringVisitor.asString( + appendWhereClause(DatabendToStringVisitor.asString( new DatabendNewExpressionGenerator(globalState).generateExpression(DatabendDataType.BOOLEAN))); DatabendErrors.addExpressionErrors(errors); } - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/databend/gen/DatabendInsertGenerator.java b/src/sqlancer/databend/gen/DatabendInsertGenerator.java index 340e494ef..13ee80e95 100644 --- a/src/sqlancer/databend/gen/DatabendInsertGenerator.java +++ b/src/sqlancer/databend/gen/DatabendInsertGenerator.java @@ -1,12 +1,9 @@ package sqlancer.databend.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; -import sqlancer.common.schema.AbstractTableColumn; import sqlancer.databend.DatabendErrors; import sqlancer.databend.DatabendProvider.DatabendGlobalState; import sqlancer.databend.DatabendSchema.DatabendColumn; @@ -16,28 +13,21 @@ public class DatabendInsertGenerator extends AbstractInsertGenerator { private final DatabendGlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); public DatabendInsertGenerator(DatabendGlobalState globalState) { this.globalState = globalState; } public static SQLQueryAdapter getQuery(DatabendGlobalState globalState) { - return new DatabendInsertGenerator(globalState).generate(); + return new DatabendInsertGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { - sb.append("INSERT INTO "); + @Override + public void buildStatement() { DatabendTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); - sb.append(table.getName()); - sb.append("("); - sb.append(columns.stream().map(AbstractTableColumn::getName).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); + buildInsertInto(table.getName(), columns); DatabendErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/databend/gen/DatabendTableGenerator.java b/src/sqlancer/databend/gen/DatabendTableGenerator.java index 514d740d0..2c3416538 100644 --- a/src/sqlancer/databend/gen/DatabendTableGenerator.java +++ b/src/sqlancer/databend/gen/DatabendTableGenerator.java @@ -4,8 +4,8 @@ import java.util.List; import sqlancer.Randomly; +import sqlancer.common.gen.AbstractTableGenerator; import sqlancer.common.gen.TypedExpressionGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.databend.DatabendErrors; import sqlancer.databend.DatabendProvider.DatabendGlobalState; @@ -15,44 +15,49 @@ import sqlancer.databend.DatabendToStringVisitor; import sqlancer.databend.ast.DatabendExpression; -public class DatabendTableGenerator { +public class DatabendTableGenerator extends AbstractTableGenerator { + + private DatabendGlobalState globalState; + private TypedExpressionGenerator gen; + + public DatabendTableGenerator() { + this.canAffectSchema = true; + } public SQLQueryAdapter getQuery(DatabendGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); + this.globalState = globalState; + return getStatement(); + } + + @Override + public void buildStatement() { DatabendErrors.addExpressionErrors(errors); - StringBuilder sb = new StringBuilder(); String tableName = globalState.getSchema().getFreeTableName(); - sb.append("CREATE TABLE "); - sb.append(tableName); - sb.append("("); + appendCreateTable(tableName); List columns = getNewColumns(); - TypedExpressionGenerator gen = new DatabendNewExpressionGenerator( - globalState).setColumns(columns); - for (int i = 0; i < columns.size(); i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(columns.get(i).getName()); - sb.append(" "); - sb.append(columns.get(i).getType()); + gen = new DatabendNewExpressionGenerator(globalState).setColumns(columns); + appendColumnDefinitions(columns); + } - if (globalState.getDbmsSpecificOptions().testNotNullConstraints - && Randomly.getBooleanWithRatherLowProbability()) { - sb.append(" NOT NULL"); - } else { - sb.append(" NULL"); // Databend 默认字段为非空,这个将它默认设置为允许空 - } + @Override + protected void appendColumnDefinition(DatabendColumn column) { + sb.append(column.getName()); + sb.append(" "); + sb.append(column.getType()); - if (Randomly.getBoolean() && globalState.getDbmsSpecificOptions().testDefaultValues) { - sb.append(" DEFAULT("); - sb.append(DatabendToStringVisitor.asString(// 常量类型于字段类型等同 - gen.generateConstant(columns.get(i).getType().getPrimitiveDataType()))); - sb.append(")"); - } + if (globalState.getDbmsSpecificOptions().testNotNullConstraints + && Randomly.getBooleanWithRatherLowProbability()) { + sb.append(" NOT NULL"); + } else { + sb.append(" NULL"); // Databend 默认字段为非空,这个将它默认设置为允许空 } - sb.append(")"); - return new SQLQueryAdapter(sb.toString(), errors, true); + if (Randomly.getBoolean() && globalState.getDbmsSpecificOptions().testDefaultValues) { + sb.append(" DEFAULT("); + sb.append(DatabendToStringVisitor.asString(// 常量类型于字段类型等同 + gen.generateConstant(column.getType().getPrimitiveDataType()))); + sb.append(")"); + } } private static List getNewColumns() { diff --git a/src/sqlancer/databend/test/tlp/DatabendQueryPartitioningAggregateTester.java b/src/sqlancer/databend/test/tlp/DatabendQueryPartitioningAggregateTester.java index 0ba0d9c86..ee3656413 100644 --- a/src/sqlancer/databend/test/tlp/DatabendQueryPartitioningAggregateTester.java +++ b/src/sqlancer/databend/test/tlp/DatabendQueryPartitioningAggregateTester.java @@ -44,9 +44,11 @@ public DatabendQueryPartitioningAggregateTester(DatabendGlobalState state) { @Override public void check() throws SQLException { super.check(); - DatabendAggregateFunction aggregateFunction = Randomly.fromOptions(DatabendAggregateFunction.MAX, - DatabendAggregateFunction.MIN, DatabendAggregateFunction.SUM, DatabendAggregateFunction.COUNT, - DatabendAggregateFunction.AVG/* , DatabendAggregateFunction.STDDEV_POP */); + List aggregateFunctions = new ArrayList<>( + List.of(DatabendAggregateFunction.MAX, DatabendAggregateFunction.MIN, DatabendAggregateFunction.SUM, + DatabendAggregateFunction.COUNT, DatabendAggregateFunction.AVG + /* , DatabendAggregateFunction.STDDEV_POP */)); + DatabendAggregateFunction aggregateFunction = Randomly.fromList(aggregateFunctions); DatabendFunctionOperation aggregate = (DatabendAggregateOperation) gen .generateArgsForAggregate(aggregateFunction); List fetchColumns = new ArrayList<>(); diff --git a/src/sqlancer/datafusion/DataFusionSchema.java b/src/sqlancer/datafusion/DataFusionSchema.java index b9f00a02e..d02e80c30 100644 --- a/src/sqlancer/datafusion/DataFusionSchema.java +++ b/src/sqlancer/datafusion/DataFusionSchema.java @@ -37,7 +37,7 @@ public static DataFusionSchema fromConnection(SQLConnection con, String database for (String tableName : tableNames) { List databaseColumns = getTableColumns(con, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); DataFusionTable t = new DataFusionTable(tableName, databaseColumns, isView); for (DataFusionColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/datafusion/gen/DataFusionInsertGenerator.java b/src/sqlancer/datafusion/gen/DataFusionInsertGenerator.java index 1ee00dd50..36a178791 100644 --- a/src/sqlancer/datafusion/gen/DataFusionInsertGenerator.java +++ b/src/sqlancer/datafusion/gen/DataFusionInsertGenerator.java @@ -1,11 +1,9 @@ package sqlancer.datafusion.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.IgnoreMeException; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.datafusion.DataFusionProvider.DataFusionGlobalState; import sqlancer.datafusion.DataFusionSchema.DataFusionColumn; @@ -15,33 +13,24 @@ public class DataFusionInsertGenerator extends AbstractInsertGenerator { private final DataFusionGlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); + private final DataFusionTable targetTable; - public DataFusionInsertGenerator(DataFusionGlobalState globalState) { + public DataFusionInsertGenerator(DataFusionGlobalState globalState, DataFusionTable targetTable) { this.globalState = globalState; + this.targetTable = targetTable; } public static SQLQueryAdapter getQuery(DataFusionGlobalState globalState, DataFusionTable targetTable) { - return new DataFusionInsertGenerator(globalState).generate(targetTable); + return new DataFusionInsertGenerator(globalState, targetTable).getStatement(); } - private SQLQueryAdapter generate(DataFusionTable targetTable) { - // `sb` is a global `StringBuilder` for current insert query - sb.append("INSERT INTO "); - + @Override + public void buildStatement() { if (targetTable.getColumns().isEmpty()) { throw new IgnoreMeException(); } List columns = targetTable.getRandomNonEmptyColumnSubset(); - - sb.append(targetTable.getName()); - sb.append("("); - sb.append(columns.stream().map(c -> c.getName()).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); // will finally call `insertValue()` to generate random value - - return new SQLQueryAdapter(sb.toString(), errors); + buildInsertInto(targetTable.getName(), columns); } @Override diff --git a/src/sqlancer/datafusion/server/datafusion_server/Cargo.toml b/src/sqlancer/datafusion/server/datafusion_server/Cargo.toml index cd8b85e1d..332a88e30 100644 --- a/src/sqlancer/datafusion/server/datafusion_server/Cargo.toml +++ b/src/sqlancer/datafusion/server/datafusion_server/Cargo.toml @@ -16,7 +16,7 @@ arrow-schema = { version = "52.1.0", default-features = false } arrow-string = { version = "52.1.0", default-features = false } async-trait = "0.1.73" bytes = "1.4" -chrono = { version = "0.4.34", default-features = false } +chrono = { version = ">=0.4.34, <0.4.40", default-features = false } dashmap = "5.5.0" # This version is for SQLancer CI run datafusion = { version = "40.0.0" } diff --git a/src/sqlancer/datafusion/server/datafusion_server/src/main.rs b/src/sqlancer/datafusion/server/datafusion_server/src/main.rs index 13ec73e96..057c34883 100644 --- a/src/sqlancer/datafusion/server/datafusion_server/src/main.rs +++ b/src/sqlancer/datafusion/server/datafusion_server/src/main.rs @@ -215,6 +215,11 @@ impl FlightSqlService for FlightSqlServiceImpl { let mut ctx_guard = self.ctx.lock().await; // Use `lock()` for async Mutex *ctx_guard = new_ctx; + + // Clear leaked state from previous round + self.statements.clear(); + self.results.clear(); + self.contexts.clear(); } // no authentication actually takes place here // see Ballista implementation for example of basic auth diff --git a/src/sqlancer/doris/DorisSchema.java b/src/sqlancer/doris/DorisSchema.java index 76697fad7..70a61ee62 100644 --- a/src/sqlancer/doris/DorisSchema.java +++ b/src/sqlancer/doris/DorisSchema.java @@ -569,7 +569,7 @@ public static DorisSchema fromConnection(SQLConnection con, String databaseName) continue; } List databaseColumns = getTableColumns(con, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); DorisTable t = new DorisTable(tableName, databaseColumns, isView); for (DorisColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/doris/gen/DorisDeleteGenerator.java b/src/sqlancer/doris/gen/DorisDeleteGenerator.java index 27f369aec..b155e0381 100644 --- a/src/sqlancer/doris/gen/DorisDeleteGenerator.java +++ b/src/sqlancer/doris/gen/DorisDeleteGenerator.java @@ -1,7 +1,7 @@ package sqlancer.doris.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.doris.DorisErrors; import sqlancer.doris.DorisProvider.DorisGlobalState; @@ -9,23 +9,27 @@ import sqlancer.doris.DorisSchema.DorisTable; import sqlancer.doris.visitor.DorisToStringVisitor; -public final class DorisDeleteGenerator { +public final class DorisDeleteGenerator extends AbstractDeleteGenerator { - private DorisDeleteGenerator() { + private final DorisGlobalState globalState; + + private DorisDeleteGenerator(DorisGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter generate(DorisGlobalState globalState) { - StringBuilder sb = new StringBuilder("DELETE FROM "); - ExpectedErrors errors = new ExpectedErrors(); + return new DorisDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { DorisTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - sb.append(table.getName()); + appendDeleteFromTable(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(DorisToStringVisitor.asString(new DorisNewExpressionGenerator(globalState) + appendWhereClause(DorisToStringVisitor.asString(new DorisNewExpressionGenerator(globalState) .setColumns(table.getColumns()).generateExpression(DorisSchema.DorisDataType.BOOLEAN))); DorisErrors.addExpressionErrors(errors); } - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/doris/gen/DorisIndexGenerator.java b/src/sqlancer/doris/gen/DorisIndexGenerator.java index 5e56bb192..308c09c3f 100644 --- a/src/sqlancer/doris/gen/DorisIndexGenerator.java +++ b/src/sqlancer/doris/gen/DorisIndexGenerator.java @@ -5,42 +5,45 @@ import sqlancer.IgnoreMeException; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.doris.DorisProvider.DorisGlobalState; import sqlancer.doris.DorisSchema.DorisColumn; import sqlancer.doris.DorisSchema.DorisTable; -public final class DorisIndexGenerator { +public class DorisIndexGenerator extends AbstractIndexGenerator { - private DorisIndexGenerator() { + private final DorisGlobalState globalState; + + public DorisIndexGenerator(DorisGlobalState globalState) { + this.globalState = globalState; + this.canAffectSchema = true; } public static SQLQueryAdapter getQuery(DorisGlobalState globalState) throws SQLException { if (globalState.getSchema().getIndexCount() > globalState.getDbmsSpecificOptions().maxNumIndexes) { throw new IgnoreMeException(); } - ExpectedErrors errors = new ExpectedErrors(); + return new DorisIndexGenerator(globalState).getStatement(); + } + @Override + public void buildStatement() { DorisTable randomTable = globalState.getSchema().getRandomTable(t -> !t.isView()); - String indexName = globalState.getSchema().getFreeIndexName(); - StringBuilder sb = new StringBuilder("CREATE "); - sb.append("INDEX "); + appendCreateIndex(false); if (Randomly.getBoolean()) { sb.append("IF NOT EXISTS "); } - sb.append(indexName); + sb.append(globalState.getSchema().getFreeIndexName()); sb.append(" ON "); sb.append(randomTable.getName()); - sb.append("("); - int nr = 1; // Doris Only support CREATE_INDEX on single column and index type is BITMAP; - List subset = Randomly.extractNrRandomColumns(randomTable.getColumns(), nr); - sb.append(subset.get(0).getName()); - sb.append(") "); + // Doris only supports CREATE INDEX on a single column; index type is BITMAP + List subset = Randomly.extractNrRandomColumns(randomTable.getColumns(), 1); + appendIndexColumnList(subset, false); + sb.append(" "); if (Randomly.getBoolean()) { sb.append("USING BITMAP "); } - return new SQLQueryAdapter(sb.toString(), errors, true); } } diff --git a/src/sqlancer/doris/gen/DorisInsertGenerator.java b/src/sqlancer/doris/gen/DorisInsertGenerator.java index 50dc5cdec..e05bc85fd 100644 --- a/src/sqlancer/doris/gen/DorisInsertGenerator.java +++ b/src/sqlancer/doris/gen/DorisInsertGenerator.java @@ -1,11 +1,9 @@ package sqlancer.doris.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.Randomly; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.doris.DorisErrors; import sqlancer.doris.DorisProvider.DorisGlobalState; @@ -16,28 +14,21 @@ public class DorisInsertGenerator extends AbstractInsertGenerator { private final DorisGlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); public DorisInsertGenerator(DorisGlobalState globalState) { this.globalState = globalState; } public static SQLQueryAdapter getQuery(DorisGlobalState globalState) { - return new DorisInsertGenerator(globalState).generate(); + return new DorisInsertGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { - sb.append("INSERT INTO "); + @Override + public void buildStatement() { DorisTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - List columns = table.getRandomNonEmptyInsertColumns(); - sb.append(table.getName()); - sb.append(" ("); - sb.append(columns.stream().map(c -> c.getName()).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); + List columns = table.getRandomNonEmptyColumnSubset(); + buildInsertInto(table.getName(), columns); DorisErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/doris/gen/DorisUpdateGenerator.java b/src/sqlancer/doris/gen/DorisUpdateGenerator.java index 906173921..93f835eff 100644 --- a/src/sqlancer/doris/gen/DorisUpdateGenerator.java +++ b/src/sqlancer/doris/gen/DorisUpdateGenerator.java @@ -23,10 +23,11 @@ private DorisUpdateGenerator(DorisGlobalState globalState) { } public static SQLQueryAdapter getQuery(DorisGlobalState globalState) { - return new DorisUpdateGenerator(globalState).generate(); + return new DorisUpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { DorisTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new DorisNewExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -34,10 +35,8 @@ private SQLQueryAdapter generate() { sb.append(table.getName()); sb.append(" SET "); updateColumns(columns); - sb.append(" WHERE "); - sb.append(DorisToStringVisitor.asString(gen.generateExpression(DorisSchema.DorisDataType.BOOLEAN))); + appendWhereClause(DorisToStringVisitor.asString(gen.generateExpression(DorisSchema.DorisDataType.BOOLEAN))); DorisErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/duckdb/DuckDBSchema.java b/src/sqlancer/duckdb/DuckDBSchema.java index 379576fd8..857b1e008 100644 --- a/src/sqlancer/duckdb/DuckDBSchema.java +++ b/src/sqlancer/duckdb/DuckDBSchema.java @@ -4,7 +4,6 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import sqlancer.IgnoreMeException; @@ -133,6 +132,7 @@ public DuckDBColumn(String name, DuckDBCompositeDataType columnType, boolean isP this.isNullable = isNullable; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } @@ -218,8 +218,8 @@ private static DuckDBCompositeDataType getColumnType(String typeString) { public static class DuckDBTable extends AbstractRelationalTable { - public DuckDBTable(String tableName, List columns, boolean isView) { - super(tableName, columns, Collections.emptyList(), isView); + public DuckDBTable(String tableName, List columns, List indexes, boolean isView) { + super(tableName, columns, indexes, isView); } } @@ -232,8 +232,9 @@ public static DuckDBSchema fromConnection(SQLConnection con, String databaseName continue; // TODO: unexpected? } List databaseColumns = getTableColumns(con, tableName); - boolean isView = tableName.startsWith("v"); - DuckDBTable t = new DuckDBTable(tableName, databaseColumns, isView); + boolean isView = matchesViewName(tableName); + List indexes = getIndexes(con, tableName); + DuckDBTable t = new DuckDBTable(tableName, databaseColumns, indexes, isView); for (DuckDBColumn c : databaseColumns) { c.setTable(t); } @@ -243,6 +244,21 @@ public static DuckDBSchema fromConnection(SQLConnection con, String databaseName return new DuckDBSchema(databaseTables); } + private static List getIndexes(SQLConnection con, String tableName) throws SQLException { + List indexes = new ArrayList<>(); + try (Statement s = con.createStatement()) { + try (ResultSet rs = s.executeQuery(String.format( + "SELECT index_name FROM duckdb_indexes() WHERE database_name = current_database() AND table_name = '%s';", + tableName))) { + while (rs.next()) { + String indexName = rs.getString("INDEX_NAME"); + indexes.add(TableIndex.create(indexName)); + } + } + } + return indexes; + } + private static List getTableNames(SQLConnection con) throws SQLException { List tableNames = new ArrayList<>(); try (Statement s = con.createStatement()) { diff --git a/src/sqlancer/duckdb/gen/DuckDBDeleteGenerator.java b/src/sqlancer/duckdb/gen/DuckDBDeleteGenerator.java index 5fadce30a..9f4f4ed6a 100644 --- a/src/sqlancer/duckdb/gen/DuckDBDeleteGenerator.java +++ b/src/sqlancer/duckdb/gen/DuckDBDeleteGenerator.java @@ -1,30 +1,34 @@ package sqlancer.duckdb.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.duckdb.DuckDBErrors; import sqlancer.duckdb.DuckDBProvider.DuckDBGlobalState; import sqlancer.duckdb.DuckDBSchema.DuckDBTable; import sqlancer.duckdb.DuckDBToStringVisitor; -public final class DuckDBDeleteGenerator { +public final class DuckDBDeleteGenerator extends AbstractDeleteGenerator { - private DuckDBDeleteGenerator() { + private final DuckDBGlobalState globalState; + + private DuckDBDeleteGenerator(DuckDBGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter generate(DuckDBGlobalState globalState) { - StringBuilder sb = new StringBuilder("DELETE FROM "); - ExpectedErrors errors = new ExpectedErrors(); + return new DuckDBDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { DuckDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - sb.append(table.getName()); + appendDeleteFromTable(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(DuckDBToStringVisitor.asString( + appendWhereClause(DuckDBToStringVisitor.asString( new DuckDBExpressionGenerator(globalState).setColumns(table.getColumns()).generateExpression())); } DuckDBErrors.addExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/duckdb/gen/DuckDBIndexGenerator.java b/src/sqlancer/duckdb/gen/DuckDBIndexGenerator.java index 597fda19f..bd42b64bd 100644 --- a/src/sqlancer/duckdb/gen/DuckDBIndexGenerator.java +++ b/src/sqlancer/duckdb/gen/DuckDBIndexGenerator.java @@ -3,27 +3,33 @@ import java.util.List; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.duckdb.DuckDBProvider.DuckDBGlobalState; import sqlancer.duckdb.DuckDBSchema.DuckDBColumn; import sqlancer.duckdb.DuckDBSchema.DuckDBTable; -public final class DuckDBIndexGenerator { +public class DuckDBIndexGenerator extends AbstractIndexGenerator { - private DuckDBIndexGenerator() { + private final DuckDBGlobalState globalState; + + public DuckDBIndexGenerator(DuckDBGlobalState globalState) { + this.globalState = globalState; + this.canAffectSchema = true; } public static SQLQueryAdapter getQuery(DuckDBGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); - sb.append("CREATE "); - if (Randomly.getBoolean()) { + return new DuckDBIndexGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { + boolean unique = Randomly.getBoolean(); + if (unique) { errors.add("Data contains duplicates on indexed column(s)"); - sb.append("UNIQUE "); } - sb.append("INDEX "); - sb.append(Randomly.fromOptions("i0", "i1", "i2", "i3", "i4")); // cannot query this information + appendCreateIndex(unique); + sb.append(globalState.getSchema().getFreeIndexName()); sb.append(" ON "); DuckDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); sb.append(table.getName()); @@ -40,11 +46,9 @@ public static SQLQueryAdapter getQuery(DuckDBGlobalState globalState) { } } sb.append(")"); - errors.add("already exists!"); if (globalState.getDbmsSpecificOptions().testRowid) { errors.add("cannot create an index on the rowid"); } - return new SQLQueryAdapter(sb.toString(), errors, true); } } diff --git a/src/sqlancer/duckdb/gen/DuckDBInsertGenerator.java b/src/sqlancer/duckdb/gen/DuckDBInsertGenerator.java index 6793d2b51..e8b122a51 100644 --- a/src/sqlancer/duckdb/gen/DuckDBInsertGenerator.java +++ b/src/sqlancer/duckdb/gen/DuckDBInsertGenerator.java @@ -1,11 +1,9 @@ package sqlancer.duckdb.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.Randomly; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.duckdb.DuckDBErrors; import sqlancer.duckdb.DuckDBProvider.DuckDBGlobalState; @@ -16,28 +14,21 @@ public class DuckDBInsertGenerator extends AbstractInsertGenerator { private final DuckDBGlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); public DuckDBInsertGenerator(DuckDBGlobalState globalState) { this.globalState = globalState; } public static SQLQueryAdapter getQuery(DuckDBGlobalState globalState) { - return new DuckDBInsertGenerator(globalState).generate(); + return new DuckDBInsertGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { - sb.append("INSERT INTO "); + @Override + public void buildStatement() { DuckDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubsetFilter(p -> !p.getName().equals("rowid")); - sb.append(table.getName()); - sb.append("("); - sb.append(columns.stream().map(c -> c.getName()).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); + buildInsertInto(table.getName(), columns); DuckDBErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/duckdb/gen/DuckDBTableGenerator.java b/src/sqlancer/duckdb/gen/DuckDBTableGenerator.java index ea6d3537f..c8ac6f28d 100644 --- a/src/sqlancer/duckdb/gen/DuckDBTableGenerator.java +++ b/src/sqlancer/duckdb/gen/DuckDBTableGenerator.java @@ -5,8 +5,8 @@ import java.util.stream.Collectors; import sqlancer.Randomly; +import sqlancer.common.gen.AbstractTableGenerator; import sqlancer.common.gen.UntypedExpressionGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.duckdb.DuckDBErrors; import sqlancer.duckdb.DuckDBProvider.DuckDBGlobalState; @@ -16,50 +16,28 @@ import sqlancer.duckdb.DuckDBToStringVisitor; import sqlancer.duckdb.ast.DuckDBExpression; -public class DuckDBTableGenerator { +public class DuckDBTableGenerator extends AbstractTableGenerator { + + private DuckDBGlobalState globalState; + private UntypedExpressionGenerator gen; + + public DuckDBTableGenerator() { + this.canAffectSchema = true; + } public SQLQueryAdapter getQuery(DuckDBGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); + this.globalState = globalState; + return getStatement(); + } + + @Override + public void buildStatement() { String tableName = globalState.getSchema().getFreeTableName(); - sb.append("CREATE TABLE "); - sb.append(tableName); - sb.append("("); + appendCreateTable(tableName); List columns = getNewColumns(); - UntypedExpressionGenerator gen = new DuckDBExpressionGenerator(globalState) - .setColumns(columns); - for (int i = 0; i < columns.size(); i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(columns.get(i).getName()); - sb.append(" "); - sb.append(columns.get(i).getType()); - if (globalState.getDbmsSpecificOptions().testCollate && Randomly.getBooleanWithRatherLowProbability() - && columns.get(i).getType().getPrimitiveDataType() == DuckDBDataType.VARCHAR) { - sb.append(" COLLATE "); - sb.append(getRandomCollate()); - } - if (globalState.getDbmsSpecificOptions().testIndexes && Randomly.getBooleanWithRatherLowProbability()) { - sb.append(" UNIQUE"); - } - if (globalState.getDbmsSpecificOptions().testNotNullConstraints - && Randomly.getBooleanWithRatherLowProbability()) { - sb.append(" NOT NULL"); - } - if (globalState.getDbmsSpecificOptions().testCheckConstraints - && Randomly.getBooleanWithRatherLowProbability()) { - sb.append(" CHECK("); - sb.append(DuckDBToStringVisitor.asString(gen.generateExpression())); - DuckDBErrors.addExpressionErrors(errors); - sb.append(")"); - } - if (Randomly.getBoolean() && globalState.getDbmsSpecificOptions().testDefaultValues) { - sb.append(" DEFAULT("); - sb.append(DuckDBToStringVisitor.asString(gen.generateConstant())); - sb.append(")"); - } - } + gen = new DuckDBExpressionGenerator(globalState).setColumns(columns); + sb.append("("); + appendColumnDefinitionList(columns); if (globalState.getDbmsSpecificOptions().testIndexes && Randomly.getBoolean()) { errors.add("Invalid type for index"); List primaryKeyColumns = Randomly.nonEmptySubset(columns); @@ -68,7 +46,37 @@ public SQLQueryAdapter getQuery(DuckDBGlobalState globalState) { sb.append(")"); } sb.append(")"); - return new SQLQueryAdapter(sb.toString(), errors, true); + } + + @Override + protected void appendColumnDefinition(DuckDBColumn column) { + sb.append(column.getName()); + sb.append(" "); + sb.append(column.getType()); + if (globalState.getDbmsSpecificOptions().testCollate && Randomly.getBooleanWithRatherLowProbability() + && column.getType().getPrimitiveDataType() == DuckDBDataType.VARCHAR) { + sb.append(" COLLATE "); + sb.append(getRandomCollate()); + } + if (globalState.getDbmsSpecificOptions().testIndexes && Randomly.getBooleanWithRatherLowProbability()) { + sb.append(" UNIQUE"); + } + if (globalState.getDbmsSpecificOptions().testNotNullConstraints + && Randomly.getBooleanWithRatherLowProbability()) { + sb.append(" NOT NULL"); + } + if (globalState.getDbmsSpecificOptions().testCheckConstraints + && Randomly.getBooleanWithRatherLowProbability()) { + sb.append(" CHECK("); + sb.append(DuckDBToStringVisitor.asString(gen.generateExpression())); + DuckDBErrors.addExpressionErrors(errors); + sb.append(")"); + } + if (Randomly.getBoolean() && globalState.getDbmsSpecificOptions().testDefaultValues) { + sb.append(" DEFAULT("); + sb.append(DuckDBToStringVisitor.asString(gen.generateConstant())); + sb.append(")"); + } } public static String getRandomCollate() { diff --git a/src/sqlancer/duckdb/gen/DuckDBUpdateGenerator.java b/src/sqlancer/duckdb/gen/DuckDBUpdateGenerator.java index b4ffd0140..8e2ddd047 100644 --- a/src/sqlancer/duckdb/gen/DuckDBUpdateGenerator.java +++ b/src/sqlancer/duckdb/gen/DuckDBUpdateGenerator.java @@ -22,10 +22,11 @@ private DuckDBUpdateGenerator(DuckDBGlobalState globalState) { } public static SQLQueryAdapter getQuery(DuckDBGlobalState globalState) { - return new DuckDBUpdateGenerator(globalState).generate(); + return new DuckDBUpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { DuckDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubsetFilter(p -> !p.getName().equals("rowid")); gen = new DuckDBExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -34,7 +35,6 @@ private SQLQueryAdapter generate() { sb.append(" SET "); updateColumns(columns); DuckDBErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/h2/H2DeleteGenerator.java b/src/sqlancer/h2/H2DeleteGenerator.java index 58afdcb48..291ee99c2 100644 --- a/src/sqlancer/h2/H2DeleteGenerator.java +++ b/src/sqlancer/h2/H2DeleteGenerator.java @@ -1,33 +1,36 @@ package sqlancer.h2; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.h2.H2Provider.H2GlobalState; import sqlancer.h2.H2Schema.H2Table; -public final class H2DeleteGenerator { +public final class H2DeleteGenerator extends AbstractDeleteGenerator { - private H2DeleteGenerator() { + private final H2GlobalState globalState; + + private H2DeleteGenerator(H2GlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter getQuery(H2GlobalState globalState) { - StringBuilder sb = new StringBuilder("DELETE FROM "); - ExpectedErrors errors = new ExpectedErrors(); + return new H2DeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { H2Table table = globalState.getSchema().getRandomTable(t -> !t.isView()); - sb.append(table.getName()); + appendDeleteFromTable(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(H2ToStringVisitor.asString( + appendWhereClause(H2ToStringVisitor.asString( new H2ExpressionGenerator(globalState).setColumns(table.getColumns()).generateExpression())); } if (Randomly.getBoolean()) { - sb.append(" LIMIT "); - sb.append(H2ToStringVisitor.asString(new H2ExpressionGenerator(globalState).generateConstant())); + appendLimitClause(H2ToStringVisitor.asString(new H2ExpressionGenerator(globalState).generateConstant())); } H2Errors.addExpressionErrors(errors); H2Errors.addDeleteErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/h2/H2InsertGenerator.java b/src/sqlancer/h2/H2InsertGenerator.java index c4e559b0e..3bd7552d8 100644 --- a/src/sqlancer/h2/H2InsertGenerator.java +++ b/src/sqlancer/h2/H2InsertGenerator.java @@ -5,7 +5,6 @@ import sqlancer.Randomly; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.h2.H2Provider.H2GlobalState; import sqlancer.h2.H2Schema.H2Column; @@ -14,7 +13,6 @@ public class H2InsertGenerator extends AbstractInsertGenerator { private final H2GlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); private final H2ExpressionGenerator gen; public H2InsertGenerator(H2GlobalState globalState) { @@ -23,10 +21,11 @@ public H2InsertGenerator(H2GlobalState globalState) { } public static SQLQueryAdapter getQuery(H2GlobalState globalState) { - return new H2InsertGenerator(globalState).generate(); + return new H2InsertGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { boolean mergeInto = false; // Randomly.getBooleanWithRatherLowProbability(); if (mergeInto) { sb.append("MERGE INTO "); @@ -39,9 +38,7 @@ private SQLQueryAdapter generate() { H2Table table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); sb.append(table.getName()); - sb.append("("); - sb.append(columns.stream().map(c -> c.getName()).collect(Collectors.joining(", "))); - sb.append(")"); + appendColumnList(columns); if (mergeInto && Randomly.getBoolean()) { sb.append(" KEY("); sb.append(table.getRandomNonEmptyColumnSubset().stream().map(c -> c.getName()) @@ -52,7 +49,6 @@ private SQLQueryAdapter generate() { insertColumns(columns); H2Errors.addInsertErrors(errors); H2Errors.addExpressionErrors(errors); // generated columns - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/h2/H2UpdateGenerator.java b/src/sqlancer/h2/H2UpdateGenerator.java index 158621409..05e348038 100644 --- a/src/sqlancer/h2/H2UpdateGenerator.java +++ b/src/sqlancer/h2/H2UpdateGenerator.java @@ -19,10 +19,11 @@ private H2UpdateGenerator(H2GlobalState globalState) { } public static SQLQueryAdapter getQuery(H2GlobalState globalState) { - return new H2UpdateGenerator(globalState).generate(); + return new H2UpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { H2Table table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new H2ExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -33,11 +34,9 @@ private SQLQueryAdapter generate() { H2Errors.addInsertErrors(errors); H2Errors.addDeleteErrors(errors); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(H2ToStringVisitor.asString(gen.generateExpression())); + appendWhereClause(H2ToStringVisitor.asString(gen.generateExpression())); } H2Errors.addExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/hive/HiveBugs.java b/src/sqlancer/hive/HiveBugs.java new file mode 100644 index 000000000..43d42ce0b --- /dev/null +++ b/src/sqlancer/hive/HiveBugs.java @@ -0,0 +1,41 @@ +package sqlancer.hive; + +// do not make the fields final to avoid warnings +public final class HiveBugs { + + // Incorrect IS NULL evaluation for negation of string concatenation involving column references. + // -(c || 'x') evaluates to NULL at runtime, but IS NULL incorrectly returns false. + // The optimizer's nullability inference for GenericUDFOPNegative does not account for + // runtime conversion failures producing NULL from non-null input. + // Reproduce: CREATE TABLE t(c DOUBLE); INSERT INTO t VALUES(1.0); + // SELECT (-(c || 'x')) IS NULL FROM t; -- returns false, expected true + // Affects: 4.0.1, 4.2.0 + public static boolean bugNegationNullability = true; + + // Non-boolean expressions (CAST to non-boolean, FLOOR, ROUND, arithmetic) silently + // return 0 rows for all three TLP partitions when used as WHERE predicates. + // Hive requires BOOLEAN in WHERE but does not error; instead it returns empty results. + // Reproduce: CREATE TABLE t(c INT); INSERT INTO t VALUES(1); + // SELECT * FROM t WHERE FLOOR(1); -- returns 0 rows, expected 1 + // Affects: 4.0.1, 4.2.0 + public static boolean bugNonBooleanWhereClause = true; + + // IN operator with boolean sub-expressions involving IS NULL evaluates incorrectly, + // returning 0 rows for all three TLP partitions. + // Reproduce: CREATE TABLE t(c BOOLEAN); INSERT INTO t VALUES(true),(false); + // SELECT * FROM t WHERE (c != c) IN ((false) IS NULL); -- returns 0, expected 2 + // Affects: 4.0.1, 4.2.0 + public static boolean bugInBooleanEvaluation = true; + + // BETWEEN with mixed boolean/numeric types has incorrect TLP evaluation. + // The IS NULL partition misses rows due to wrong nullability inference. + // Reproduce: CREATE TABLE t(c DOUBLE); INSERT INTO t VALUES(0.5),(1.5); + // SELECT * FROM t WHERE (c NOT IN (true)) NOT BETWEEN 0.01 AND c; + // -- TLP partitions lose rows + // Affects: 4.0.1, 4.2.0 + public static boolean bugBetweenMixedTypes = true; + + private HiveBugs() { + } + +} diff --git a/src/sqlancer/hive/HiveSchema.java b/src/sqlancer/hive/HiveSchema.java index 822eea163..8733d5caa 100644 --- a/src/sqlancer/hive/HiveSchema.java +++ b/src/sqlancer/hive/HiveSchema.java @@ -59,7 +59,7 @@ public static HiveSchema fromConnection(SQLConnection con, String databaseName) List tableNames = getTableNames(con); for (String tableName : tableNames) { List databaseColumns = getTableColumns(con, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); HiveTable t = new HiveTable(tableName, databaseColumns, isView); for (HiveColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/hive/gen/HiveExpressionGenerator.java b/src/sqlancer/hive/gen/HiveExpressionGenerator.java index 9f3de2514..92154873c 100644 --- a/src/sqlancer/hive/gen/HiveExpressionGenerator.java +++ b/src/sqlancer/hive/gen/HiveExpressionGenerator.java @@ -5,12 +5,14 @@ import java.util.List; import java.util.stream.Collectors; +import sqlancer.IgnoreMeException; import sqlancer.Randomly; import sqlancer.common.ast.BinaryOperatorNode.Operator; import sqlancer.common.ast.newast.NewOrderingTerm.Ordering; import sqlancer.common.gen.TLPWhereGenerator; import sqlancer.common.gen.UntypedExpressionGenerator; import sqlancer.common.schema.AbstractTables; +import sqlancer.hive.HiveBugs; import sqlancer.hive.HiveGlobalState; import sqlancer.hive.HiveSchema.HiveColumn; import sqlancer.hive.HiveSchema.HiveDataType; @@ -75,12 +77,27 @@ private HiveExpression generateExpressionInternal(int depth) throws AssertionErr } List possibleOptions = new ArrayList<>(Arrays.asList(Expression.values())); - // TODO: remove some of the possible expression types according to options. + if (HiveBugs.bugNonBooleanWhereClause) { + possibleOptions.remove(Expression.CAST); + possibleOptions.remove(Expression.FUNC); + possibleOptions.remove(Expression.BINARY_ARITHMETIC); + } + if (HiveBugs.bugInBooleanEvaluation) { + possibleOptions.remove(Expression.IN); + } + if (HiveBugs.bugBetweenMixedTypes) { + possibleOptions.remove(Expression.BETWEEN); + } Expression expr = Randomly.fromList(possibleOptions); switch (expr) { case UNARY_PREFIX: - return new HiveUnaryPrefixOperation(generateExpression(depth + 1), HiveUnaryPrefixOperator.getRandom()); + HiveUnaryPrefixOperator prefixOp = HiveUnaryPrefixOperator.getRandom(); + if (HiveBugs.bugNegationNullability + && (prefixOp == HiveUnaryPrefixOperator.MINUS || prefixOp == HiveUnaryPrefixOperator.PLUS)) { + throw new IgnoreMeException(); + } + return new HiveUnaryPrefixOperation(generateExpression(depth + 1), prefixOp); case UNARY_POSTFIX: return new HiveUnaryPostfixOperation(generateExpression(depth + 1), HiveUnaryPostfixOperator.getRandom()); case BINARY_COMPARISON: diff --git a/src/sqlancer/hive/gen/HiveInsertGenerator.java b/src/sqlancer/hive/gen/HiveInsertGenerator.java index 963fafbce..8c23e4456 100644 --- a/src/sqlancer/hive/gen/HiveInsertGenerator.java +++ b/src/sqlancer/hive/gen/HiveInsertGenerator.java @@ -3,7 +3,6 @@ import java.util.List; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.hive.HiveErrors; import sqlancer.hive.HiveGlobalState; @@ -14,16 +13,16 @@ public class HiveInsertGenerator extends AbstractInsertGenerator { private final HiveGlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); private final HiveExpressionGenerator gen; public HiveInsertGenerator(HiveGlobalState globalState) { this.globalState = globalState; this.gen = new HiveExpressionGenerator(globalState); + this.canonicalizeString = false; } public static SQLQueryAdapter getQuery(HiveGlobalState globalState) { - return new HiveInsertGenerator(globalState).generate(); + return new HiveInsertGenerator(globalState).getStatement(); } @Override @@ -31,7 +30,8 @@ protected void insertValue(HiveColumn column) { sb.append(HiveToStringVisitor.asString(gen.generateConstant())); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { // Inserting values into tables from SQL. sb.append("INSERT INTO "); HiveTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); @@ -47,6 +47,5 @@ private SQLQueryAdapter generate() { insertColumns(columns); HiveErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors, false, false); } } diff --git a/src/sqlancer/hsqldb/HSQLDBSchema.java b/src/sqlancer/hsqldb/HSQLDBSchema.java index e1e1cb94b..2d41df83f 100644 --- a/src/sqlancer/hsqldb/HSQLDBSchema.java +++ b/src/sqlancer/hsqldb/HSQLDBSchema.java @@ -29,7 +29,7 @@ public static HSQLDBSchema fromConnection(SQLConnection connection, String datab continue; // TODO: unexpected? } List databaseColumns = getTableColumns(connection, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); HSQLDBSchema.HSQLDBTable t = new HSQLDBSchema.HSQLDBTable(tableName, databaseColumns, isView); for (HSQLDBSchema.HSQLDBColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/hsqldb/gen/HSQLDBInsertGenerator.java b/src/sqlancer/hsqldb/gen/HSQLDBInsertGenerator.java index 1cc132190..00d99be38 100644 --- a/src/sqlancer/hsqldb/gen/HSQLDBInsertGenerator.java +++ b/src/sqlancer/hsqldb/gen/HSQLDBInsertGenerator.java @@ -1,10 +1,8 @@ package sqlancer.hsqldb.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.hsqldb.HSQLDBProvider; import sqlancer.hsqldb.HSQLDBSchema; @@ -14,28 +12,20 @@ public class HSQLDBInsertGenerator extends AbstractInsertGenerator { private final HSQLDBProvider.HSQLDBGlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); public HSQLDBInsertGenerator(HSQLDBProvider.HSQLDBGlobalState globalState) { this.globalState = globalState; } public static SQLQueryAdapter getQuery(HSQLDBProvider.HSQLDBGlobalState globalState) { - return new HSQLDBInsertGenerator(globalState).generate(); + return new HSQLDBInsertGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { - sb.append("INSERT INTO "); + @Override + public void buildStatement() { HSQLDBSchema.HSQLDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); - sb.append(table.getName()); - sb.append("("); - sb.append(columns.stream().map(c -> c.getName()).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); - // HSQLDBErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); + buildInsertInto(table.getName(), columns); } @Override diff --git a/src/sqlancer/hsqldb/gen/HSQLDBTableGenerator.java b/src/sqlancer/hsqldb/gen/HSQLDBTableGenerator.java index 48606e9bf..30249e780 100644 --- a/src/sqlancer/hsqldb/gen/HSQLDBTableGenerator.java +++ b/src/sqlancer/hsqldb/gen/HSQLDBTableGenerator.java @@ -5,44 +5,48 @@ import javax.annotation.Nullable; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractTableGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.hsqldb.HSQLDBProvider; import sqlancer.hsqldb.HSQLDBSchema; -public class HSQLDBTableGenerator { +public class HSQLDBTableGenerator extends AbstractTableGenerator { + + private HSQLDBProvider.HSQLDBGlobalState globalState; + private String tableName; + + public HSQLDBTableGenerator() { + this.canAffectSchema = true; + } public SQLQueryAdapter getQuery(HSQLDBProvider.HSQLDBGlobalState globalState, @Nullable String tableName) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); + this.globalState = globalState; + this.tableName = tableName; + return getStatement(); + } + + @Override + public void buildStatement() { String name = tableName; - if (tableName == null) { + if (name == null) { name = globalState.getSchema().getFreeTableName(); } - sb.append("CREATE TABLE "); - if (Randomly.getBoolean()) { - sb.append("IF NOT EXISTS "); - } - sb.append(name); - sb.append("("); - List columns = getNewColumns(); - for (int i = 0; i < columns.size(); i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(columns.get(i).getName()); - sb.append(" "); - sb.append(columns.get(i).getType().getType().name()); - if (columns.get(i).getType().getSize() > 0) { - // Cannot specify size for non composite data types - sb.append("("); - sb.append(columns.get(i).getType().getSize()); - sb.append(")"); - } - } - sb.append(")"); + appendCreateTable(name, Randomly.getBoolean()); + appendColumnDefinitions(getNewColumns()); sb.append(";"); - return new SQLQueryAdapter(sb.toString(), errors, true); + } + + @Override + protected void appendColumnDefinition(HSQLDBSchema.HSQLDBColumn column) { + sb.append(column.getName()); + sb.append(" "); + sb.append(column.getType().getType().name()); + if (column.getType().getSize() > 0) { + // Cannot specify size for non composite data types + sb.append("("); + sb.append(column.getType().getSize()); + sb.append(")"); + } } private static List getNewColumns() { diff --git a/src/sqlancer/hsqldb/gen/HSQLDBUpdateGenerator.java b/src/sqlancer/hsqldb/gen/HSQLDBUpdateGenerator.java index e639e21b3..2e6081df4 100644 --- a/src/sqlancer/hsqldb/gen/HSQLDBUpdateGenerator.java +++ b/src/sqlancer/hsqldb/gen/HSQLDBUpdateGenerator.java @@ -24,10 +24,11 @@ private HSQLDBUpdateGenerator(HSQLDBProvider.HSQLDBGlobalState globalState) { } public static SQLQueryAdapter getQuery(HSQLDBProvider.HSQLDBGlobalState globalState) { - return new HSQLDBUpdateGenerator(globalState).generate(); + return new HSQLDBUpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { HSQLDBSchema.HSQLDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new HSQLDBExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -36,13 +37,11 @@ private SQLQueryAdapter generate() { sb.append(" SET "); updateColumns(columns); if (Randomly.getBooleanWithSmallProbability()) { - sb.append(" WHERE "); - sb.append(HSQLDBToStringVisitor.asString( + appendWhereClause(HSQLDBToStringVisitor.asString( gen.generateExpression(HSQLDBCompositeDataType.getRandomWithType(HSQLDBDataType.BOOLEAN)))); errors.add("data type of expression is not boolean"); HSQLDBErrors.addExpressionErrors(errors); } - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/mariadb/MariaDBSchema.java b/src/sqlancer/mariadb/MariaDBSchema.java index 808653b25..7f7656d76 100644 --- a/src/sqlancer/mariadb/MariaDBSchema.java +++ b/src/sqlancer/mariadb/MariaDBSchema.java @@ -51,6 +51,7 @@ public int getPrecision() { return precision; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } diff --git a/src/sqlancer/mariadb/gen/MariaDBDeleteGenerator.java b/src/sqlancer/mariadb/gen/MariaDBDeleteGenerator.java index 6d85eb891..da1651662 100644 --- a/src/sqlancer/mariadb/gen/MariaDBDeleteGenerator.java +++ b/src/sqlancer/mariadb/gen/MariaDBDeleteGenerator.java @@ -3,7 +3,7 @@ import java.util.Collections; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.common.schema.AbstractTables; import sqlancer.mariadb.MariaDBSchema; @@ -11,12 +11,22 @@ import sqlancer.mariadb.MariaDBSchema.MariaDBTable; import sqlancer.mariadb.ast.MariaDBVisitor; -public final class MariaDBDeleteGenerator { +public final class MariaDBDeleteGenerator extends AbstractDeleteGenerator { - private MariaDBDeleteGenerator() { + private final MariaDBSchema schema; + private final Randomly r; + + private MariaDBDeleteGenerator(MariaDBSchema schema, Randomly r) { + this.schema = schema; + this.r = r; } public static SQLQueryAdapter delete(MariaDBSchema schema, Randomly r) { + return new MariaDBDeleteGenerator(schema, r).getStatement(); + } + + @Override + public void buildStatement() { MariaDBTable table = schema.getRandomTable(); MariaDBExpressionGenerator expressionGenerator = new MariaDBExpressionGenerator(r); @@ -25,15 +35,13 @@ public static SQLQueryAdapter delete(MariaDBSchema schema, Randomly r) { Collections.singletonList(table)); expressionGenerator.setTablesAndColumns(tablesAndColumns); - ExpectedErrors errors = new ExpectedErrors(); - errors.add("foreign key constraint fails"); errors.add("cannot delete or update a parent row"); errors.add("Data truncated"); errors.add("Division by 0"); errors.add("Incorrect value"); - StringBuilder sb = new StringBuilder("DELETE"); + sb.append("DELETE"); if (Randomly.getBooleanWithRatherLowProbability()) { sb.append(" LOW_PRIORITY"); @@ -49,12 +57,13 @@ public static SQLQueryAdapter delete(MariaDBSchema schema, Randomly r) { sb.append(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); + String condition; if (Randomly.getBooleanWithRatherLowProbability()) { - sb.append(MariaDBVisitor.asString(MariaDBExpressionGenerator.getRandomConstant(r))); + condition = MariaDBVisitor.asString(MariaDBExpressionGenerator.getRandomConstant(r)); } else { - sb.append(MariaDBVisitor.asString(expressionGenerator.getRandomExpression())); + condition = MariaDBVisitor.asString(expressionGenerator.getRandomExpression()); } + appendWhereClause(condition); } // ORDER BY + LIMIT @@ -67,27 +76,24 @@ public static SQLQueryAdapter delete(MariaDBSchema schema, Randomly r) { } if (Randomly.getBooleanWithRatherLowProbability()) { - sb.append(" LIMIT "); - sb.append(Randomly.getNotCachedInteger(1, 10)); + appendLimitClause(Randomly.getNotCachedInteger(1, 10)); } // RETURNING clause (MariaDB >= 10.5) if (Randomly.getBooleanWithRatherLowProbability()) { - sb.append(" RETURNING "); + String expression; if (Randomly.getBooleanWithRatherLowProbability()) { - sb.append(MariaDBVisitor.asString(MariaDBExpressionGenerator.getRandomConstant(r))); + expression = MariaDBVisitor.asString(MariaDBExpressionGenerator.getRandomConstant(r)); } else { - sb.append(MariaDBVisitor.asString(expressionGenerator.getRandomExpression())); + expression = MariaDBVisitor.asString(expressionGenerator.getRandomExpression()); } + appendReturningClause(expression); } - String query = sb.toString(); - if (query.contains("RLIKE") || query.contains("REGEXP")) { + if (sb.toString().contains("RLIKE") || sb.toString().contains("REGEXP")) { errors.add("Regex error"); errors.add("quantifier does not follow a repeatable item"); errors.add("Got error"); } - - return new SQLQueryAdapter(query, errors); } } diff --git a/src/sqlancer/mariadb/gen/MariaDBIndexGenerator.java b/src/sqlancer/mariadb/gen/MariaDBIndexGenerator.java index 1ba3fbd4d..1fb6f10b3 100644 --- a/src/sqlancer/mariadb/gen/MariaDBIndexGenerator.java +++ b/src/sqlancer/mariadb/gen/MariaDBIndexGenerator.java @@ -1,31 +1,36 @@ package sqlancer.mariadb.gen; -import java.util.List; - import sqlancer.Randomly; import sqlancer.common.DBMSCommon; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.mariadb.MariaDBSchema; import sqlancer.mariadb.MariaDBSchema.MariaDBColumn; import sqlancer.mariadb.MariaDBSchema.MariaDBTable; -public final class MariaDBIndexGenerator { +public class MariaDBIndexGenerator extends AbstractIndexGenerator { + + private final MariaDBSchema schema; - private MariaDBIndexGenerator() { + public MariaDBIndexGenerator(MariaDBSchema schema) { + this.schema = schema; + this.canAffectSchema = true; } public static SQLQueryAdapter generate(MariaDBSchema s) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder("CREATE "); + return new MariaDBIndexGenerator(s).getStatement(); + } + + @Override + public void buildStatement() { errors.add("Key/Index cannot be defined on a virtual generated column"); errors.add("Specified key was too long"); - if (Randomly.getBoolean()) { + boolean unique = Randomly.getBoolean(); + if (unique) { errors.add("Duplicate entry"); errors.add("Key/Index cannot be defined on a virtual generated column"); - sb.append("UNIQUE "); } - sb.append("INDEX "); + appendCreateIndex(unique); sb.append("i"); sb.append(DBMSCommon.createColumnName(Randomly.smallNumber())); if (Randomly.getBoolean()) { @@ -34,28 +39,9 @@ public static SQLQueryAdapter generate(MariaDBSchema s) { } sb.append(" ON "); - MariaDBTable randomTable = s.getRandomTable(); + MariaDBTable randomTable = schema.getRandomTable(); sb.append(randomTable.getName()); - sb.append("("); - List columns = Randomly.nonEmptySubset(randomTable.getColumns()); - for (int i = 0; i < columns.size(); i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(columns.get(i).getName()); - if (Randomly.getBoolean()) { - sb.append(" "); - sb.append(Randomly.fromOptions("ASC", "DESC")); - } - } - sb.append(")"); - // if (Randomly.getBoolean()) { - // sb.append(" ALGORITHM="); - // sb.append(Randomly.fromOptions("DEFAULT", "INPLACE", "COPY", "NOCOPY", "INSTANT")); - // errors.add("is not supported for this operation"); - // } - - return new SQLQueryAdapter(sb.toString(), errors, true); + appendIndexColumnList(Randomly.nonEmptySubset(randomTable.getColumns()), true); } } diff --git a/src/sqlancer/materialize/MaterializeBugs.java b/src/sqlancer/materialize/MaterializeBugs.java new file mode 100644 index 000000000..a7611a925 --- /dev/null +++ b/src/sqlancer/materialize/MaterializeBugs.java @@ -0,0 +1,12 @@ +package sqlancer.materialize; + +// do not make the fields final to avoid warnings +public final class MaterializeBugs { + + // Tables or columns may be missing when reading information_schema shortly after creation + public static boolean bugSchemaReadIncomplete = true; + + private MaterializeBugs() { + } + +} diff --git a/src/sqlancer/materialize/MaterializeGlobalState.java b/src/sqlancer/materialize/MaterializeGlobalState.java index 77cbe5d14..7ab963c03 100644 --- a/src/sqlancer/materialize/MaterializeGlobalState.java +++ b/src/sqlancer/materialize/MaterializeGlobalState.java @@ -27,6 +27,8 @@ public class MaterializeGlobalState extends SQLGlobalState functionsAndTypes = new HashMap<>(); private List allowedFunctionTypes = Arrays.asList(IMMUTABLE, STABLE, VOLATILE); + private int lastKnownTableCount; + private int readSchemaCallCount; @Override public void setConnection(SQLConnection con) { @@ -266,6 +268,31 @@ public String getRandomTableAccessMethod() { @Override public MaterializeSchema readSchema() throws SQLException { + if (MaterializeBugs.bugSchemaReadIncomplete) { + // Workaround for a suspected Materialize bug where tables or columns may be + // missing when reading the schema; retry until stable. + readSchemaCallCount++; + for (int tries = 0; tries < 30; tries++) { + MaterializeSchema schema = MaterializeSchema.fromConnection(getConnection(), getDatabaseName()); + boolean hasTableWithEmptyColumns = schema.getDatabaseTables().stream() + .anyMatch(t -> t.getColumns().isEmpty()); + boolean tableCountRegressed = schema.getDatabaseTables().size() < lastKnownTableCount; + boolean suspiciouslyEmpty = readSchemaCallCount > 1 && schema.getDatabaseTables().isEmpty(); + if (!hasTableWithEmptyColumns && !tableCountRegressed && !suspiciouslyEmpty) { + lastKnownTableCount = schema.getDatabaseTables().size(); + return schema; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + MaterializeSchema schema = MaterializeSchema.fromConnection(getConnection(), getDatabaseName()); + lastKnownTableCount = schema.getDatabaseTables().size(); + return schema; + } return MaterializeSchema.fromConnection(getConnection(), getDatabaseName()); } diff --git a/src/sqlancer/materialize/MaterializeProvider.java b/src/sqlancer/materialize/MaterializeProvider.java index 5b18b1dfb..e7bdb4c4f 100644 --- a/src/sqlancer/materialize/MaterializeProvider.java +++ b/src/sqlancer/materialize/MaterializeProvider.java @@ -240,12 +240,17 @@ protected void readFunctions(MaterializeGlobalState globalState) throws SQLExcep } protected void createTables(MaterializeGlobalState globalState, int numTables) throws Exception { - while (globalState.getSchema().getDatabaseTables().size() < numTables) { + int existingTables = globalState.getSchema().getDatabaseTables().size(); + int createdTables = 0; + int nextTableIndex = existingTables; + while (existingTables + createdTables < numTables) { try { - String tableName = DBMSCommon.createTableName(globalState.getSchema().getDatabaseTables().size()); + String tableName = DBMSCommon.createTableName(nextTableIndex++); SQLQueryAdapter createTable = MaterializeTableGenerator.generate(tableName, globalState.getSchema(), generateOnlyKnown, globalState); - globalState.executeStatement(createTable); + if (globalState.executeStatement(createTable)) { + createdTables++; + } } catch (IgnoreMeException e) { } diff --git a/src/sqlancer/materialize/gen/MaterializeDeleteGenerator.java b/src/sqlancer/materialize/gen/MaterializeDeleteGenerator.java index ded3e53f4..a5483ee1d 100644 --- a/src/sqlancer/materialize/gen/MaterializeDeleteGenerator.java +++ b/src/sqlancer/materialize/gen/MaterializeDeleteGenerator.java @@ -1,30 +1,34 @@ package sqlancer.materialize.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.materialize.MaterializeGlobalState; import sqlancer.materialize.MaterializeSchema.MaterializeDataType; import sqlancer.materialize.MaterializeSchema.MaterializeTable; import sqlancer.materialize.MaterializeVisitor; -public final class MaterializeDeleteGenerator { +public final class MaterializeDeleteGenerator extends AbstractDeleteGenerator { - private MaterializeDeleteGenerator() { + private final MaterializeGlobalState globalState; + + private MaterializeDeleteGenerator(MaterializeGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter create(MaterializeGlobalState globalState) { + return new MaterializeDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { MaterializeTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - ExpectedErrors errors = new ExpectedErrors(); errors.add("violates foreign key constraint"); errors.add("violates not-null constraint"); errors.add("could not determine which collation to use for string comparison"); - StringBuilder sb = new StringBuilder("DELETE FROM"); - sb.append(" "); - sb.append(table.getName()); + appendDeleteFromTable(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(MaterializeVisitor.asString(MaterializeExpressionGenerator.generateExpression(globalState, + appendWhereClause(MaterializeVisitor.asString(MaterializeExpressionGenerator.generateExpression(globalState, table.getColumns(), MaterializeDataType.BOOLEAN))); } MaterializeCommon.addCommonExpressionErrors(errors); @@ -32,7 +36,6 @@ public static SQLQueryAdapter create(MaterializeGlobalState globalState) { errors.add("does not support casting"); errors.add("invalid input syntax for"); errors.add("division by zero"); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/materialize/gen/MaterializeExpressionGenerator.java b/src/sqlancer/materialize/gen/MaterializeExpressionGenerator.java index d61e452f8..f7ff76305 100644 --- a/src/sqlancer/materialize/gen/MaterializeExpressionGenerator.java +++ b/src/sqlancer/materialize/gen/MaterializeExpressionGenerator.java @@ -600,6 +600,7 @@ public String generateOptimizedQueryString(MaterializeSelect select, Materialize } select.setSelectType(SelectType.ALL); } + select.setWhereClause(whereCondition); return select.asString(); } diff --git a/src/sqlancer/materialize/gen/MaterializeIndexGenerator.java b/src/sqlancer/materialize/gen/MaterializeIndexGenerator.java index 9d7a91b50..a63353f1f 100644 --- a/src/sqlancer/materialize/gen/MaterializeIndexGenerator.java +++ b/src/sqlancer/materialize/gen/MaterializeIndexGenerator.java @@ -1,14 +1,18 @@ package sqlancer.materialize.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.materialize.MaterializeGlobalState; +import sqlancer.materialize.MaterializeSchema.MaterializeColumn; import sqlancer.materialize.MaterializeSchema.MaterializeTable; -public final class MaterializeIndexGenerator { +public class MaterializeIndexGenerator extends AbstractIndexGenerator { - private MaterializeIndexGenerator() { + private final MaterializeGlobalState globalState; + + public MaterializeIndexGenerator(MaterializeGlobalState globalState) { + this.globalState = globalState; } public enum IndexType { @@ -16,17 +20,18 @@ public enum IndexType { } public static SQLQueryAdapter generate(MaterializeGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); - sb.append("CREATE"); - sb.append(" INDEX "); + return new MaterializeIndexGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { + appendCreateIndex(false); MaterializeTable randomTable = globalState.getSchema().getRandomTable(t -> !t.isView()); // TODO: materialized // views sb.append(MaterializeCommon.getFreeIndexName(globalState.getSchema())); sb.append(" ON "); sb.append(randomTable.getName()); - IndexType method; - method = IndexType.BTREE; + IndexType method = IndexType.BTREE; sb.append("("); if (method == IndexType.HASH) { @@ -75,6 +80,5 @@ public static SQLQueryAdapter generate(MaterializeGlobalState globalState) { errors.add("result of range difference would not be contiguous"); errors.add("which is part of the partition key"); MaterializeCommon.addCommonExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/materialize/gen/MaterializeInsertGenerator.java b/src/sqlancer/materialize/gen/MaterializeInsertGenerator.java index 01015514b..7a5374b95 100644 --- a/src/sqlancer/materialize/gen/MaterializeInsertGenerator.java +++ b/src/sqlancer/materialize/gen/MaterializeInsertGenerator.java @@ -26,7 +26,7 @@ public static SQLQueryAdapter insert(MaterializeGlobalState globalState) { MaterializeCommon.addCommonExpressionErrors(errors); errors.add("multiple assignments to same column"); errors.add("violates foreign key constraint"); - errors.add("value too long for type character varying"); + errors.add("value too long for type character"); errors.add("conflicting key value violates exclusion constraint"); errors.add("violates not-null constraint"); errors.add("current transaction is aborted"); diff --git a/src/sqlancer/materialize/gen/MaterializeTableGenerator.java b/src/sqlancer/materialize/gen/MaterializeTableGenerator.java index f132f7370..c6772db47 100644 --- a/src/sqlancer/materialize/gen/MaterializeTableGenerator.java +++ b/src/sqlancer/materialize/gen/MaterializeTableGenerator.java @@ -52,6 +52,7 @@ public MaterializeTableGenerator(String tableName, MaterializeSchema newSchema, errors.add("no collation was derived for partition key column"); errors.add("inherits from generated column but specifies identity"); errors.add("inherits from generated column but specifies default"); + errors.add("already exists"); MaterializeCommon.addCommonExpressionErrors(errors); MaterializeCommon.addCommonTableErrors(errors); } diff --git a/src/sqlancer/materialize/gen/MaterializeUpdateGenerator.java b/src/sqlancer/materialize/gen/MaterializeUpdateGenerator.java index 1b7e69208..abd288f99 100644 --- a/src/sqlancer/materialize/gen/MaterializeUpdateGenerator.java +++ b/src/sqlancer/materialize/gen/MaterializeUpdateGenerator.java @@ -20,6 +20,7 @@ public final class MaterializeUpdateGenerator extends AbstractUpdateGenerator t.isInsertable()); List columns = randomTable.getRandomNonEmptyColumnSubset(); sb.append("UPDATE "); @@ -50,13 +52,10 @@ private SQLQueryAdapter generate() { errors.add("but expression is of type"); MaterializeCommon.addCommonExpressionErrors(errors); if (!Randomly.getBooleanWithSmallProbability()) { - sb.append(" WHERE "); MaterializeExpression where = MaterializeExpressionGenerator.generateExpression(globalState, randomTable.getColumns(), MaterializeDataType.BOOLEAN); - sb.append(MaterializeVisitor.asString(where)); + appendWhereClause(MaterializeVisitor.asString(where)); } - - return new SQLQueryAdapter(sb.toString(), errors, true); } @Override diff --git a/src/sqlancer/materialize/gen/MaterializeViewGenerator.java b/src/sqlancer/materialize/gen/MaterializeViewGenerator.java index c8cd0e93f..e3cb8ff29 100644 --- a/src/sqlancer/materialize/gen/MaterializeViewGenerator.java +++ b/src/sqlancer/materialize/gen/MaterializeViewGenerator.java @@ -30,19 +30,11 @@ public static SQLQueryAdapter create(MaterializeGlobalState globalState) { materialized = false; } sb.append(" VIEW "); - int i = 0; - String[] name = new String[1]; - while (true) { - name[0] = "v" + i++; - if (globalState.getSchema().getDatabaseTables().stream() - .noneMatch(tab -> tab.getName().contentEquals(name[0]))) { - break; - } - } - sb.append(name[0]); + String name = globalState.getSchema().getFreeViewName(); + sb.append(name); sb.append("("); int nrColumns = Randomly.smallNumber() + 1; - for (i = 0; i < nrColumns; i++) { + for (int i = 0; i < nrColumns; i++) { if (i != 0) { sb.append(", "); } diff --git a/src/sqlancer/mysql/MySQLBugs.java b/src/sqlancer/mysql/MySQLBugs.java index 8cb8a3391..e4fae3cd7 100644 --- a/src/sqlancer/mysql/MySQLBugs.java +++ b/src/sqlancer/mysql/MySQLBugs.java @@ -3,12 +3,8 @@ // do not make the fields final to avoid warnings public final class MySQLBugs { - // https://bugs.mysql.com/bug.php?id=99127 0.9 > t0.c0 malfunctions when c0 is - // an INT UNSIGNED - public static boolean bug99127 = true; - // https://bugs.mysql.com/99182 BETWEEN malfunctions for DECIMAL and TEXT - public static boolean bug99181 = true; + public static boolean bug99182 = true; // https://bugs.mysql.com/bug.php?id=99183 public static boolean bug99183 = true; @@ -37,6 +33,19 @@ public final class MySQLBugs { // https://bugs.mysql.com/bug.php?id=114534 public static boolean bug114534 = true; + // https://bugs.mysql.com/bug.php?id=120710 + // Inserting a NULL and a value which rounds to 0 into a DECIMAL column causes result set mismatch. + public static boolean bug120710 = true; + + // https://bugs.mysql.com/bug.php?id=120711 + // Creating an index on an integer-type column, then inserting a value which rounds to 1, causes result set + // mismatch. + public static boolean bug120711 = true; + + // https://bugs.mysql.com/bug.php?id=120712 + // Creating an index in between two NULL inserts causes inconsistent CERT result. + public static boolean bug120712 = true; + private MySQLBugs() { } diff --git a/src/sqlancer/mysql/MySQLErrors.java b/src/sqlancer/mysql/MySQLErrors.java index f01b30e03..0b6eb8284 100644 --- a/src/sqlancer/mysql/MySQLErrors.java +++ b/src/sqlancer/mysql/MySQLErrors.java @@ -33,6 +33,7 @@ public static List getExpressionRegexErrors() { } errors.add(Pattern.compile("Unknown column '.*' in 'order clause'")); + errors.add(Pattern.compile("Unknown column '.*' in 'EXISTS subquery'")); return errors; } @@ -48,6 +49,8 @@ public static List getInsertUpdateErrors() { errors.add("doesn't have a default value"); errors.add("Data truncation"); errors.add("Incorrect integer value"); + errors.add("Incorrect FLOAT value"); + errors.add("Incorrect DOUBLE value"); errors.add("Duplicate entry"); errors.add("Data truncated for column"); errors.add("Data truncated for functional index"); @@ -62,4 +65,21 @@ public static void addInsertUpdateErrors(ExpectedErrors errors) { errors.addAll(getInsertUpdateErrors()); } + public static List getDMLErrors() { + ArrayList errors = new ArrayList<>(getInsertUpdateErrors()); + + // WHERE-clause type coercion (e.g. string -> number) is only a warning in SELECT but a hard error in + // DELETE/UPDATE under strict sql_mode (MySQL 1292). A semantics-preserving transform may benignly change + // whether it fires, so it is tolerated rather than flagged. + errors.add("Truncated incorrect"); + // Foreign key constraint failure when deleting/updating a referenced row. + errors.add("a foreign key constraint fails"); + + return errors; + } + + public static void addDMLErrors(ExpectedErrors errors) { + errors.addAll(getDMLErrors()); + } + } diff --git a/src/sqlancer/mysql/MySQLGlobalState.java b/src/sqlancer/mysql/MySQLGlobalState.java index 10132b57c..a34861cbd 100644 --- a/src/sqlancer/mysql/MySQLGlobalState.java +++ b/src/sqlancer/mysql/MySQLGlobalState.java @@ -16,4 +16,13 @@ public boolean usesPQS() { return getDbmsSpecificOptions().oracles.stream().anyMatch(o -> o == MySQLOracleFactory.PQS); } + public boolean usesEET() { + return getDbmsSpecificOptions().getTestOracleFactory().stream() + .anyMatch(o -> o == MySQLOracleFactory.EET || o == MySQLOracleFactory.EET_DML); + } + + public boolean usesEETDML() { + return getDbmsSpecificOptions().getTestOracleFactory().stream().anyMatch(o -> o == MySQLOracleFactory.EET_DML); + } + } diff --git a/src/sqlancer/mysql/MySQLOracleFactory.java b/src/sqlancer/mysql/MySQLOracleFactory.java index 83e08677a..8b37efb13 100644 --- a/src/sqlancer/mysql/MySQLOracleFactory.java +++ b/src/sqlancer/mysql/MySQLOracleFactory.java @@ -5,6 +5,8 @@ import sqlancer.OracleFactory; import sqlancer.common.oracle.CERTOracle; +import sqlancer.common.oracle.EETDMLOracle; +import sqlancer.common.oracle.EETOracle; import sqlancer.common.oracle.TLPWhereOracle; import sqlancer.common.oracle.TestOracle; import sqlancer.common.query.ExpectedErrors; @@ -82,5 +84,27 @@ public TestOracle create(MySQLGlobalState globalState) throws public TestOracle create(MySQLGlobalState globalState) throws SQLException { return new MySQLDQEOracle(globalState); } + }, + EET { + @Override + public TestOracle create(MySQLGlobalState globalState) throws SQLException { + MySQLExpressionGenerator gen = new MySQLExpressionGenerator(globalState); + ExpectedErrors expectedErrors = ExpectedErrors.newErrors().with(MySQLErrors.getExpressionErrors()) + .withRegex(MySQLErrors.getExpressionRegexErrors()).build(); + return new EETOracle<>(globalState, gen, expectedErrors); + } + }, + EET_DML { + @Override + public TestOracle create(MySQLGlobalState globalState) throws SQLException { + MySQLExpressionGenerator gen = new MySQLExpressionGenerator(globalState); + ExpectedErrors expectedErrors = ExpectedErrors.newErrors().with(MySQLErrors.getExpressionErrors()) + .withRegex(MySQLErrors.getExpressionRegexErrors()) + // The DML statements and the row-identity setup (adding/stamping the auxiliary column) touch rows, + // so they can raise the full range of DML errors — e.g. functional-index maintenance truncation — + // beyond the SELECT-based expression errors. + .with(MySQLErrors.getDMLErrors()).build(); + return new EETDMLOracle<>(globalState, gen, expectedErrors); + } }; } diff --git a/src/sqlancer/mysql/MySQLSchema.java b/src/sqlancer/mysql/MySQLSchema.java index 0384f34df..ab0ba4543 100644 --- a/src/sqlancer/mysql/MySQLSchema.java +++ b/src/sqlancer/mysql/MySQLSchema.java @@ -57,6 +57,7 @@ public static class MySQLColumn extends AbstractTableColumn c.isPrimaryKey()); - } - } public static final class MySQLIndex extends TableIndex { @@ -279,8 +282,10 @@ private static List getTableColumns(SQLConnection con, String table String columnName = rs.getString("COLUMN_NAME"); String dataType = rs.getString("DATA_TYPE"); int precision = rs.getInt("NUMERIC_PRECISION"); + int scale = rs.getInt("NUMERIC_SCALE"); boolean isPrimaryKey = rs.getString("COLUMN_KEY").equals("PRI"); - MySQLColumn c = new MySQLColumn(columnName, getColumnType(dataType), isPrimaryKey, precision); + MySQLColumn c = new MySQLColumn(columnName, getColumnType(dataType), isPrimaryKey, precision, + scale); columns.add(c); } } diff --git a/src/sqlancer/mysql/ast/MySQLCastOperation.java b/src/sqlancer/mysql/ast/MySQLCastOperation.java index 8ae783fa0..07c45a429 100644 --- a/src/sqlancer/mysql/ast/MySQLCastOperation.java +++ b/src/sqlancer/mysql/ast/MySQLCastOperation.java @@ -1,16 +1,89 @@ package sqlancer.mysql.ast; +import java.util.Objects; + public class MySQLCastOperation implements MySQLExpression { private final MySQLExpression expr; private final CastType type; - public enum CastType { - SIGNED, UNSIGNED; + /** + * A MySQL {@code CAST} target type. The non-{@code DECIMAL} kinds are interned singletons; {@code DECIMAL} may + * additionally carry an {@code (M, D)} precision/scale (via {@link #decimal}) so that a + * {@code CAST(... AS DECIMAL(M, D))} can reproduce a column's exact type. This is relied on by the EET oracle's + * type-pinning casts (see {@code MySQLEETTransformer}). + */ + public static final class CastType { + + // CHAR, FLOAT, DOUBLE and DECIMAL are used only by the EET oracle's type-pinning casts and are never evaluated, + // so MySQLConstant.castAs does not support them; they must not be returned by getRandom(). + public static final CastType SIGNED = new CastType(Kind.SIGNED); + public static final CastType UNSIGNED = new CastType(Kind.UNSIGNED); + public static final CastType CHAR = new CastType(Kind.CHAR); + public static final CastType FLOAT = new CastType(Kind.FLOAT); + public static final CastType DOUBLE = new CastType(Kind.DOUBLE); + public static final CastType DECIMAL = new CastType(Kind.DECIMAL); + + private enum Kind { + SIGNED, UNSIGNED, CHAR, FLOAT, DOUBLE, DECIMAL + } + + private final Kind kind; + private final Integer precision; // DECIMAL only, otherwise null + private final Integer scale; // DECIMAL only, otherwise null + + private CastType(Kind kind) { + this(kind, null, null); + } + + private CastType(Kind kind, Integer precision, Integer scale) { + this.kind = kind; + this.precision = precision; + this.scale = scale; + } + + // A DECIMAL(precision, scale) cast target. + public static CastType decimal(int precision, int scale) { + return new CastType(Kind.DECIMAL, precision, scale); + } public static CastType getRandom() { return SIGNED; - // return Randomly.fromOptions(CastType.values()); + // return Randomly.fromOptions(CastType.SIGNED, CastType.UNSIGNED); + } + + public Integer getPrecision() { + return precision; + } + + public Integer getScale() { + return scale; + } + + @Override + public String toString() { + if (precision == null) { + return kind.name(); + } + return kind.name() + "(" + precision + ", " + scale + ")"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CastType)) { + return false; + } + CastType other = (CastType) obj; + return kind == other.kind && Objects.equals(precision, other.precision) + && Objects.equals(scale, other.scale); + } + + @Override + public int hashCode() { + return Objects.hash(kind, precision, scale); } } diff --git a/src/sqlancer/mysql/ast/MySQLConstant.java b/src/sqlancer/mysql/ast/MySQLConstant.java index 2e4922f8e..5fb0698b9 100644 --- a/src/sqlancer/mysql/ast/MySQLConstant.java +++ b/src/sqlancer/mysql/ast/MySQLConstant.java @@ -68,6 +68,11 @@ public MySQLDoubleConstant(double val) { } } + @Override + public double getDouble() { + return val; + } + @Override public String getTextRepresentation() { return String.valueOf(val); @@ -381,6 +386,10 @@ public long getInt() { throw new UnsupportedOperationException(); } + public double getDouble() { + throw new UnsupportedOperationException(); + } + public boolean isSigned() { return false; } diff --git a/src/sqlancer/mysql/ast/MySQLUnaryPrefixOperation.java b/src/sqlancer/mysql/ast/MySQLUnaryPrefixOperation.java index c87048a4b..45a1fe888 100644 --- a/src/sqlancer/mysql/ast/MySQLUnaryPrefixOperation.java +++ b/src/sqlancer/mysql/ast/MySQLUnaryPrefixOperation.java @@ -62,6 +62,10 @@ public MySQLUnaryPrefixOperation(MySQLExpression expr, MySQLUnaryPrefixOperator super(expr, op); } + public MySQLUnaryPrefixOperator getOp() { + return op; + } + @Override public MySQLConstant getExpectedValue() { MySQLConstant subExprVal = expr.getExpectedValue(); diff --git a/src/sqlancer/mysql/gen/MySQLDeleteGenerator.java b/src/sqlancer/mysql/gen/MySQLDeleteGenerator.java index f12c23a72..551264c4a 100644 --- a/src/sqlancer/mysql/gen/MySQLDeleteGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLDeleteGenerator.java @@ -3,16 +3,15 @@ import java.util.Arrays; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.mysql.MySQLErrors; import sqlancer.mysql.MySQLGlobalState; import sqlancer.mysql.MySQLSchema.MySQLTable; import sqlancer.mysql.MySQLVisitor; -public class MySQLDeleteGenerator { +public class MySQLDeleteGenerator extends AbstractDeleteGenerator { - private final StringBuilder sb = new StringBuilder(); private final MySQLGlobalState globalState; public MySQLDeleteGenerator(MySQLGlobalState globalState) { @@ -20,13 +19,13 @@ public MySQLDeleteGenerator(MySQLGlobalState globalState) { } public static SQLQueryAdapter delete(MySQLGlobalState globalState) { - return new MySQLDeleteGenerator(globalState).generate(); + return new MySQLDeleteGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { MySQLTable randomTable = globalState.getSchema().getRandomTable(); MySQLExpressionGenerator gen = new MySQLExpressionGenerator(globalState).setColumns(randomTable.getColumns()); - ExpectedErrors errors = new ExpectedErrors(); sb.append("DELETE"); if (Randomly.getBoolean()) { sb.append(" LOW_PRIORITY"); @@ -41,8 +40,7 @@ private SQLQueryAdapter generate() { sb.append(" FROM "); sb.append(randomTable.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(MySQLVisitor.asString(gen.generateExpression())); + appendWhereClause(MySQLVisitor.asString(gen.generateExpression())); MySQLErrors.addExpressionErrors(errors); } errors.addAll(Arrays.asList("doesn't have this option", @@ -51,7 +49,6 @@ private SQLQueryAdapter generate() { */, "Truncated incorrect INTEGER value", "Truncated incorrect DECIMAL value", "Data truncated for functional index")); // TODO: support ORDER BY - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java index 98641ab26..baea11f65 100644 --- a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java @@ -1,7 +1,9 @@ package sqlancer.mysql.gen; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -9,14 +11,18 @@ import sqlancer.IgnoreMeException; import sqlancer.Randomly; import sqlancer.common.gen.CERTGenerator; +import sqlancer.common.gen.EETDMLGenerator; +import sqlancer.common.gen.EETGenerator; import sqlancer.common.gen.TLPWhereGenerator; import sqlancer.common.gen.UntypedExpressionGenerator; +import sqlancer.common.oracle.EETTransformer; import sqlancer.common.schema.AbstractTables; import sqlancer.mysql.MySQLBugs; import sqlancer.mysql.MySQLGlobalState; import sqlancer.mysql.MySQLSchema.MySQLColumn; import sqlancer.mysql.MySQLSchema.MySQLRowValue; import sqlancer.mysql.MySQLSchema.MySQLTable; +import sqlancer.mysql.MySQLVisitor; import sqlancer.mysql.ast.MySQLAggregate; import sqlancer.mysql.ast.MySQLAggregate.MySQLAggregateFunction; import sqlancer.mysql.ast.MySQLBetweenOperation; @@ -45,10 +51,13 @@ import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; +import sqlancer.mysql.oracle.MySQLEETTransformer; public class MySQLExpressionGenerator extends UntypedExpressionGenerator implements TLPWhereGenerator, - CERTGenerator { + CERTGenerator, + EETGenerator, + EETDMLGenerator { private final MySQLGlobalState state; private MySQLRowValue rowVal; @@ -112,7 +121,7 @@ public MySQLExpression generateExpression(int depth) { case EXISTS: return getExists(); case BETWEEN_OPERATOR: - if (MySQLBugs.bug99181) { + if (MySQLBugs.bug99182) { // TODO: there are a number of bugs that are triggered by the BETWEEN operator throw new IgnoreMeException(); } @@ -218,6 +227,22 @@ public List generateOrderBys() { return newOrderBys; } + public MySQLAggregate generateAggregate() { + MySQLAggregateFunction func = Randomly.fromOptions(MySQLAggregateFunction.values()); + + if (func.isVariadic()) { + int nrExprs = Randomly.smallNumber() + 1; + List exprs = IntStream.range(0, nrExprs).mapToObj(index -> generateExpression()) + .collect(Collectors.toList()); + + return new MySQLAggregate(exprs, func); + } else { + return new MySQLAggregate(List.of(generateExpression()), func); + } + } + + // --- Shared oracle infrastructure (TLPWhere / CERT / EET) --- + @Override public MySQLExpressionGenerator setTablesAndColumns(AbstractTables tables) { this.columns = tables.getColumns(); @@ -231,6 +256,26 @@ public MySQLExpression generateBooleanExpression() { return generateExpression(); } + @Override + public List> generateSetAssignments() { + List> assignments = new ArrayList<>(); + for (MySQLColumn column : Randomly.nonEmptySubset(columns)) { + // As with the normal UPDATE workload, the value is an arbitrary expression (not type-matched to the + // column); + // any resulting type/range error is on the oracle's expected-error allow-list. + assignments.add(new AbstractMap.SimpleEntry<>(column, generateExpression())); + } + return assignments; + } + + @Override + public List generateInsertValues() { + // One value per content column, in schema order (aligned with the INSERT column list). As with the normal + // INSERT workload, each value is an arbitrary expression (not type-matched to the column); any resulting + // type/range/constraint error is on the oracle's expected-error allow-list. + return columns.stream().map(c -> generateExpression()).collect(Collectors.toList()); + } + @Override public MySQLSelect generateSelect() { return new MySQLSelect(); @@ -251,23 +296,13 @@ public List generateFetchColumns(boolean shouldCreateDummy) { return columns.stream().map(c -> new MySQLColumnReference(c, null)).collect(Collectors.toList()); } + // --- CERT oracle --- + @Override public String generateExplainQuery(MySQLSelect select) { - return "EXPLAIN " + select.asString(); - } - - public MySQLAggregate generateAggregate() { - MySQLAggregateFunction func = Randomly.fromOptions(MySQLAggregateFunction.values()); - - if (func.isVariadic()) { - int nrExprs = Randomly.smallNumber() + 1; - List exprs = IntStream.range(0, nrExprs).mapToObj(index -> generateExpression()) - .collect(Collectors.toList()); - - return new MySQLAggregate(exprs, func); - } else { - return new MySQLAggregate(List.of(generateExpression()), func); - } + return "EXPLAIN FORMAT=TRADITIONAL " + select.asString(); // as of MySQL 9.5.0, default EXPLAIN format changed + // from TRADITIONAL to TREE, hence TRADITIONAL must + // now be specified } @Override @@ -353,4 +388,40 @@ boolean mutateOr(MySQLSelect select) { return true; } } + + // --- EET oracle (including DML) --- + + @Override + public EETTransformer createTransformer() { + return new MySQLEETTransformer(this); + } + + // --- EET DML only --- + + @Override + public String asString(MySQLExpression expr) { + return MySQLVisitor.asString(expr); + } + + @Override + public String stampRowIdsStatement(MySQLTable table) { + // MySQL's UUID() gives each existing row a distinct value in a single statement. Stamping happens once, before + // both rolled-back statement runs, so both observe identical identifiers; the standard-SQL statements (add/drop + // column, delete/update, snapshot, transaction control) use EETDMLGenerator's defaults. + return String.format("UPDATE %s SET %s = UUID()", table.getName(), ROW_ID_COLUMN); + } + + @Override + public String rowIdColumnType() { + // Holds a 36-character UUID string produced by stampRowIdsStatement. + return "VARCHAR(36)"; + } + + @Override + public String insertedRowIdExpression() { + // The source row's identifier with its dashes removed: deterministic (identical across both runs) and unique + // per + // source row. Fits the identifier column's VARCHAR(36). + return String.format("REPLACE(%s, '-', '')", ROW_ID_COLUMN); + } } diff --git a/src/sqlancer/mysql/gen/MySQLHintGenerator.java b/src/sqlancer/mysql/gen/MySQLHintGenerator.java index 141aea279..dc6138b23 100644 --- a/src/sqlancer/mysql/gen/MySQLHintGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLHintGenerator.java @@ -170,7 +170,7 @@ private void indexesHint(String string) { MySQLTable table = Randomly.fromList(tables); List allIndexes = table.getIndexes(); sb.append(table.getName()); - sb.append(", "); + sb.append(" "); if (allIndexes.isEmpty()) { sb.append("PRIMARY"); } else { diff --git a/src/sqlancer/mysql/gen/MySQLInsertGenerator.java b/src/sqlancer/mysql/gen/MySQLInsertGenerator.java index 86083fd2d..0e464dead 100644 --- a/src/sqlancer/mysql/gen/MySQLInsertGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLInsertGenerator.java @@ -7,11 +7,16 @@ import sqlancer.Randomly; import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.mysql.MySQLBugs; import sqlancer.mysql.MySQLErrors; import sqlancer.mysql.MySQLGlobalState; +import sqlancer.mysql.MySQLOracleFactory; import sqlancer.mysql.MySQLSchema.MySQLColumn; +import sqlancer.mysql.MySQLSchema.MySQLDataType; import sqlancer.mysql.MySQLSchema.MySQLTable; import sqlancer.mysql.MySQLVisitor; +import sqlancer.mysql.ast.MySQLConstant; +import sqlancer.mysql.ast.MySQLExpression; public class MySQLInsertGenerator { @@ -19,6 +24,8 @@ public class MySQLInsertGenerator { private final StringBuilder sb = new StringBuilder(); private final ExpectedErrors errors = new ExpectedErrors(); private final MySQLGlobalState globalState; + private static final int MAX_REGENERATION_ATTEMPTS = 100; // for regenerating expression until valid (for bug + // workarounds) public MySQLInsertGenerator(MySQLGlobalState globalState, MySQLTable table) { this.globalState = globalState; @@ -84,8 +91,59 @@ private SQLQueryAdapter generateInto() { if (c != 0) { sb.append(", "); } - sb.append(MySQLVisitor.asString(gen.generateConstant())); + MySQLExpression constExpr; + // loop to regenerate until expression is valid (for bug workarounds) + int regenerationAttempts = 0; + while (true) { + regenerationAttempts++; + if (regenerationAttempts > MAX_REGENERATION_ATTEMPTS) { + throw new AssertionError("Exceeded " + MAX_REGENERATION_ATTEMPTS + + " attempts while generating constant for column " + columns.get(c).getName()); + } + constExpr = gen.generateConstant(); + boolean reject = false; + // Bug workaround: for integer columns, reject values that round to 1 + if (!reject && MySQLBugs.bug120711 && columns.get(c).getType() == MySQLDataType.INT) { + if (constExpr instanceof MySQLConstant.MySQLIntConstant) { + long value = ((MySQLConstant.MySQLIntConstant) constExpr).getInt(); + reject = value == 1; + } else if (constExpr instanceof MySQLConstant.MySQLDoubleConstant) { + double value = ((MySQLConstant.MySQLDoubleConstant) constExpr).getDouble(); + reject = value >= 0.5 && value < 1.5; + } else if (constExpr instanceof MySQLConstant.MySQLTextConstant) { // reject strings, which may + // be implicitly cast to 1 + reject = true; + } + } + + // Bug workaround: for decimal columns, reject values that round to 0 + if (!reject && MySQLBugs.bug120710 && columns.get(c).getType() == MySQLDataType.DECIMAL) { + if (constExpr instanceof MySQLConstant.MySQLIntConstant) { + long value = ((MySQLConstant.MySQLIntConstant) constExpr).getInt(); + reject = value == 0; + } else if (constExpr instanceof MySQLConstant.MySQLDoubleConstant) { + double value = ((MySQLConstant.MySQLDoubleConstant) constExpr).getDouble(); + reject = value >= -0.5 && value < 0.5; + } else if (constExpr instanceof MySQLConstant.MySQLTextConstant) { // reject strings, which may + // be implicitly cast to 0 + reject = true; + } + } + + // Bug workaround: if using CERT oracle, reject NULL values + if (!reject && MySQLBugs.bug120712 + && globalState.getDbmsSpecificOptions().getTestOracleFactory().stream() + .anyMatch(o -> o == MySQLOracleFactory.CERT) + && constExpr instanceof MySQLConstant.MySQLNullConstant) { + reject = true; + } + + if (!reject) { + break; + } + } + sb.append(MySQLVisitor.asString(constExpr)); } sb.append(")"); } diff --git a/src/sqlancer/mysql/gen/MySQLSetGenerator.java b/src/sqlancer/mysql/gen/MySQLSetGenerator.java index 79333eb36..e350685ef 100644 --- a/src/sqlancer/mysql/gen/MySQLSetGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLSetGenerator.java @@ -99,7 +99,6 @@ private enum Action { SCHEMA_DEFINITION_CACHE("schema_definition_cache", (r) -> r.getLong(256, 524288), Scope.GLOBAL), // SHOW_CREATE_TABLE_VERBOSITY("show_create_table_verbosity", (r) -> Randomly.fromOptions("OFF", "ON"), Scope.GLOBAL, Scope.SESSION), // - SHOW_OLD_TEMPORALS("show_old_temporals", (r) -> Randomly.fromOptions("OFF", "ON"), Scope.GLOBAL, Scope.SESSION), /* * sort_buffer_size is commented out as a workaround for https://bugs.mysql.com/bug.php?id=95969 */ diff --git a/src/sqlancer/mysql/gen/MySQLTableGenerator.java b/src/sqlancer/mysql/gen/MySQLTableGenerator.java index bc0533295..d27d6e681 100644 --- a/src/sqlancer/mysql/gen/MySQLTableGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLTableGenerator.java @@ -13,6 +13,7 @@ import sqlancer.common.query.SQLQueryAdapter; import sqlancer.mysql.MySQLBugs; import sqlancer.mysql.MySQLGlobalState; +import sqlancer.mysql.MySQLOracleFactory; import sqlancer.mysql.MySQLSchema; import sqlancer.mysql.MySQLSchema.MySQLDataType; import sqlancer.mysql.MySQLSchema.MySQLTable.MySQLEngine; @@ -163,7 +164,15 @@ public static List getRandomTableOptions() { } private void appendTableOptions() { - List tableOptions = TableOptions.getRandomTableOptions(); + List tableOptions = new ArrayList<>(TableOptions.getRandomTableOptions()); + // The EET DML oracle rolls back each statement to compare database states, which requires a transactional + // engine. The ENGINE option already forces InnoDB when the oracle is active (see the ENGINE case below), but it + // is only emitted when randomly chosen; otherwise the table would inherit the server's default engine, which is + // not guaranteed transactional. Force the option to always be present so the engine is never left to the + // server default. + if (globalState.usesEETDML() && !tableOptions.contains(TableOptions.ENGINE)) { + tableOptions.add(TableOptions.ENGINE); + } int i = 0; for (TableOptions o : tableOptions) { if (i++ != 0) { @@ -197,7 +206,10 @@ private void appendTableOptions() { // "NDB": java.sql.SQLSyntaxErrorException: Unknown storage engine 'NDB' // "EXAMPLE": java.sql.SQLSyntaxErrorException: Unknown storage engine 'EXAMPLE' // "MERGE": java.sql.SQLException: Table 't0' is read only - String fromOptions = Randomly.fromOptions("InnoDB", "MyISAM", "MEMORY", "HEAP", "CSV", "ARCHIVE"); + // The EET DML oracle rolls back each statement to compare database states, which requires a + // transactional engine, so only InnoDB is used while it is active. + String fromOptions = globalState.usesEETDML() ? "InnoDB" + : Randomly.fromOptions("InnoDB", "MyISAM", "MEMORY", "HEAP", "CSV", "ARCHIVE"); this.engine = MySQLEngine.get(fromOptions); sb.append("ENGINE = "); sb.append(fromOptions); @@ -346,26 +358,41 @@ private void appendType(MySQLDataType randomType) { break; case FLOAT: sb.append("FLOAT"); - optionallyAddPrecisionAndScale(sb); + optionallyAddFloatingPointPrecisionAndScale(sb); break; case DOUBLE: sb.append(Randomly.fromOptions("DOUBLE", "FLOAT")); - optionallyAddPrecisionAndScale(sb); + optionallyAddFloatingPointPrecisionAndScale(sb); break; default: throw new AssertionError(); } if (randomType.isNumeric()) { - if (Randomly.getBoolean() && randomType != MySQLDataType.INT && !MySQLBugs.bug99127) { + if (Randomly.getBoolean() && randomType != MySQLDataType.INT) { sb.append(" UNSIGNED"); } - if (!globalState.usesPQS() && Randomly.getBoolean()) { + if (Randomly.getBoolean() && !globalState.getDbmsSpecificOptions().getTestOracleFactory().stream() + .anyMatch(o -> o == MySQLOracleFactory.TLP_WHERE || o == MySQLOracleFactory.PQS + || o == MySQLOracleFactory.DQP || o == MySQLOracleFactory.EET + || o == MySQLOracleFactory.EET_DML)) { sb.append(" ZEROFILL"); } } } - public static void optionallyAddPrecisionAndScale(StringBuilder sb) { + // FLOAT(M, D)/DOUBLE(M, D) is deprecated and cannot be reproduced as a CAST target, so the EET oracle's type + // inference relies on FLOAT/DOUBLE columns being created without (M, D) (see MySQLEETTransformer#inferColumnType); + // it is therefore omitted while EET is active. DECIMAL(M, D) has no such restriction: the EET oracle tracks its + // (M, D) and reproduces it via CAST(... AS DECIMAL(M, D)), so it keeps using optionallyAddPrecisionAndScale. + private void optionallyAddFloatingPointPrecisionAndScale(StringBuilder sb) { + // Both EET oracles rely on the same type inference (MySQLEETTransformer), so both omit FLOAT(M, D)/DOUBLE(M, + // D). + if (!globalState.usesEET()) { + optionallyAddPrecisionAndScale(sb); + } + } + + private void optionallyAddPrecisionAndScale(StringBuilder sb) { if (Randomly.getBoolean() && !MySQLBugs.bug99183) { sb.append("("); // The maximum number of digits (M) for DECIMAL is 65 diff --git a/src/sqlancer/mysql/gen/MySQLUpdateGenerator.java b/src/sqlancer/mysql/gen/MySQLUpdateGenerator.java index 55ba3dd45..ad13a148a 100644 --- a/src/sqlancer/mysql/gen/MySQLUpdateGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLUpdateGenerator.java @@ -1,6 +1,5 @@ package sqlancer.mysql.gen; -import java.sql.SQLException; import java.util.List; import sqlancer.Randomly; @@ -21,11 +20,12 @@ public MySQLUpdateGenerator(MySQLGlobalState globalState) { this.globalState = globalState; } - public static SQLQueryAdapter create(MySQLGlobalState globalState) throws SQLException { - return new MySQLUpdateGenerator(globalState).generate(); + public static SQLQueryAdapter create(MySQLGlobalState globalState) { + return new MySQLUpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() throws SQLException { + @Override + public void buildStatement() { MySQLTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new MySQLExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -34,14 +34,11 @@ private SQLQueryAdapter generate() throws SQLException { sb.append(" SET "); updateColumns(columns); if (Randomly.getBoolean()) { - sb.append(" WHERE "); MySQLErrors.addExpressionErrors(errors); - sb.append(MySQLVisitor.asString(gen.generateExpression())); + appendWhereClause(MySQLVisitor.asString(gen.generateExpression())); } MySQLErrors.addInsertUpdateErrors(errors); errors.add("doesn't have this option"); - - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/mysql/gen/datadef/MySQLIndexGenerator.java b/src/sqlancer/mysql/gen/datadef/MySQLIndexGenerator.java index 550893db5..028886831 100644 --- a/src/sqlancer/mysql/gen/datadef/MySQLIndexGenerator.java +++ b/src/sqlancer/mysql/gen/datadef/MySQLIndexGenerator.java @@ -120,6 +120,8 @@ public SQLQueryAdapter create() { errors.add("Data truncated for functional index"); errors.add("used in key specification without a key length"); errors.add("Row size too large"); // seems to happen together with MIN_ROWS in the table declaration + errors.add("in the PARTITION BY KEY() clause is not supported"); // prefix key parts disallowed on + // KEY-partitioned columns return new SQLQueryAdapter(string, errors, true); } diff --git a/src/sqlancer/mysql/oracle/MySQLDQEOracle.java b/src/sqlancer/mysql/oracle/MySQLDQEOracle.java index 8ddb6f315..429ef3c89 100644 --- a/src/sqlancer/mysql/oracle/MySQLDQEOracle.java +++ b/src/sqlancer/mysql/oracle/MySQLDQEOracle.java @@ -71,6 +71,10 @@ public String generateSelectStatement(AbstractTables tables, String tableN for (MySQLColumn column : Randomly.nonEmptySubset(mySQLTables.getColumns())) { orderColumns.add(column.getFullQualifiedName()); } + // rowId tiebreaker ensures ORDER BY LIMIT is deterministic when user columns have duplicate values + for (MySQLTable table : mySQLTables.getTables()) { + orderColumns.add(table.getName() + "." + COLUMN_ROWID); + } if (Randomly.getBooleanWithRatherLowProbability()) { generateLimit = true; @@ -185,7 +189,13 @@ public void check() throws SQLException { public String compareSelectAndUpdate(SQLQueryResult selectResult, SQLQueryResult updateResult) { if (updateResult.hasEmptyErrors()) { if (!selectResult.hasEmptyErrors()) { - return "SELECT has errors, but UPDATE does not."; + // Tolerate SELECT-only discrepancy errors (e.g. 1292 raised in SELECT but not UPDATE + // due to different short-circuit evaluation paths). + boolean selectHasNonDiscrepancyErrors = selectResult.getQueryErrors().stream() + .anyMatch(e -> !isKnownSelectDMLDiscrepancy(e)); + if (selectHasNonDiscrepancyErrors) { + return "SELECT has errors, but UPDATE does not."; + } } if (!selectResult.hasSameAccessedRows(updateResult)) { return "SELECT accessed different rows from UPDATE."; @@ -201,9 +211,13 @@ public String compareSelectAndUpdate(SQLQueryResult selectResult, SQLQueryResult } // update errors should all appear in the select errors + // known SELECT/DML discrepancy errors are skipped: see KnownSelectDMLDiscrepancy for the full list. List selectErrors = new ArrayList<>(selectResult.getQueryErrors()); for (int i = 0; i < updateResult.getQueryErrors().size(); i++) { SQLQueryError updateError = updateResult.getQueryErrors().get(i); + if (isKnownSelectDMLDiscrepancy(updateError)) { + continue; + } if (!isFound(selectErrors, updateError)) { return "SELECT has different errors from UPDATE."; } @@ -247,7 +261,13 @@ private static boolean isFound(List selectErrors, SQLQueryError t public String compareSelectAndDelete(SQLQueryResult selectResult, SQLQueryResult deleteResult) { if (deleteResult.hasEmptyErrors()) { if (!selectResult.hasEmptyErrors()) { - return "SELECT has errors, but DELETE does not."; + // Tolerate SELECT-only discrepancy errors (e.g. 1292 raised in SELECT but not DELETE + // due to different short-circuit evaluation paths). + boolean selectHasNonDiscrepancyErrors = selectResult.getQueryErrors().stream() + .anyMatch(e -> !isKnownSelectDMLDiscrepancy(e)); + if (selectHasNonDiscrepancyErrors) { + return "SELECT has errors, but DELETE does not."; + } } if (!selectResult.hasSameAccessedRows(deleteResult)) { return "SELECT accessed different rows from DELETE."; @@ -263,9 +283,13 @@ public String compareSelectAndDelete(SQLQueryResult selectResult, SQLQueryResult } // delete errors should all appear in the select errors + // known SELECT/DML discrepancy errors are skipped: see KnownSelectDMLDiscrepancy for the full list. List selectErrors = new ArrayList<>(selectResult.getQueryErrors()); for (int i = 0; i < deleteResult.getQueryErrors().size(); i++) { SQLQueryError deleteError = deleteResult.getQueryErrors().get(i); + if (isKnownSelectDMLDiscrepancy(deleteError)) { + continue; + } if (!isFound(selectErrors, deleteError)) { return "SELECT has different errors from DELETE."; } @@ -349,6 +373,38 @@ private boolean hasDeleteSpecificErrors(SQLQueryResult deleteResult) { } + // Errors MySQL may raise in UPDATE/DELETE but not SELECT due to different execution paths. Acceptable + // discrepancies that should be skipped; not treated as stop errors so row comparison proceeds normally. + private enum KnownSelectDMLDiscrepancy { + // WHERE clause type coercion: MySQL may short-circuit in SELECT but evaluate fully in UPDATE/DELETE, + // raising this at ERROR level vs WARNING in SELECT. + TRUNCATED_DOUBLE_VALUE(1292), + // Same WHERE clause coercion discrepancy as TRUNCATED_DOUBLE_VALUE. + INCORRECT_COLUMN_VALUE(1366), + // Raised during functional index maintenance on UPDATE/DELETE; SELECT never writes indexes. + STORAGE_ENGINE_ERROR(1030), + // MySQL applies this memory budget differently for SELECT vs DML; the fallback full-scan still + // evaluates the WHERE predicate correctly. + RANGE_OPTIMIZER_MEM_EXCEEDED(3170), + // Raised when a functional index expression truncates a value during DML; structurally impossible in SELECT. + FUNCTIONAL_INDEX_DATA_TRUNCATED(3751); + + private final int code; + + KnownSelectDMLDiscrepancy(int code) { + this.code = code; + } + } + + private static boolean isKnownSelectDMLDiscrepancy(SQLQueryError error) { + for (KnownSelectDMLDiscrepancy discrepancy : KnownSelectDMLDiscrepancy.values()) { + if (discrepancy.code == error.getCode()) { + return true; + } + } + return false; + } + private boolean hasStopErrors(SQLQueryResult queryResult) { return queryResult.getQueryErrors().stream() .anyMatch(error -> error.getLevel() == SQLQueryError.ErrorLevel.ERROR); @@ -467,7 +523,7 @@ private List getErrors() throws SQLException { public void addAuxiliaryColumns(AbstractRelationalTable table) throws SQLException { String tableName = table.getName(); - String addColumnRowID = String.format("ALTER TABLE %s ADD %s TEXT", tableName, COLUMN_ROWID); + String addColumnRowID = String.format("ALTER TABLE %s ADD %s VARCHAR(36)", tableName, COLUMN_ROWID); new SQLQueryAdapter(addColumnRowID).execute(state, false); state.getState().getLocalState().log(addColumnRowID); diff --git a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java new file mode 100644 index 000000000..22c0302df --- /dev/null +++ b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java @@ -0,0 +1,292 @@ +package sqlancer.mysql.oracle; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import sqlancer.common.oracle.EETTransformer; +import sqlancer.mysql.MySQLSchema.MySQLColumn; +import sqlancer.mysql.ast.MySQLAggregate; +import sqlancer.mysql.ast.MySQLBetweenOperation; +import sqlancer.mysql.ast.MySQLBinaryComparisonOperation; +import sqlancer.mysql.ast.MySQLBinaryLogicalOperation; +import sqlancer.mysql.ast.MySQLBinaryLogicalOperation.MySQLBinaryLogicalOperator; +import sqlancer.mysql.ast.MySQLBinaryOperation; +import sqlancer.mysql.ast.MySQLCaseOperator; +import sqlancer.mysql.ast.MySQLCastOperation; +import sqlancer.mysql.ast.MySQLCastOperation.CastType; +import sqlancer.mysql.ast.MySQLColumnReference; +import sqlancer.mysql.ast.MySQLComputableFunction; +import sqlancer.mysql.ast.MySQLConstant; +import sqlancer.mysql.ast.MySQLExists; +import sqlancer.mysql.ast.MySQLExpression; +import sqlancer.mysql.ast.MySQLInOperation; +import sqlancer.mysql.ast.MySQLTableReference; +import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; +import sqlancer.mysql.ast.MySQLUnaryPostfixOperation.UnaryPostfixOperator; +import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; +import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; +import sqlancer.mysql.gen.MySQLExpressionGenerator; + +/** + * MySQL implementation of the {@link EETTransformer EET} tree-walker. Implements {@link #descend} to rebuild MySQL AST + * nodes from their transformed children, threading the correct boolean/scalar context into each child. + * + *

+ * MySQL's expression generator is untyped, so type inference/generation works with a subset of MySQL's CAST target + * types ({@link CastType}, which carries {@code (M, D)} for DECIMAL): {@link #inferType} conservatively classifies AST + * nodes into that domain (returning {@code null} when uncertain), and {@link #generateExpressionOfType} pins the type + * of a random expression by wrapping it in a CAST. + */ +public class MySQLEETTransformer extends EETTransformer { + + private static final boolean BOOLEAN = true; + private static final boolean SCALAR = false; + + private final MySQLExpressionGenerator gen; + + public MySQLEETTransformer(MySQLExpressionGenerator gen) { + this.gen = gen; + } + + @Override + protected MySQLExpression descend(MySQLExpression expr, boolean booleanContext) { + if (expr instanceof MySQLBinaryLogicalOperation) { + // AND/OR/XOR: both operands are evaluated in a boolean context. + MySQLBinaryLogicalOperation op = (MySQLBinaryLogicalOperation) expr; + return new MySQLBinaryLogicalOperation(transformNode(op.getLeft(), BOOLEAN, false), + transformNode(op.getRight(), BOOLEAN, false), op.getOp()); + } else if (expr instanceof MySQLBinaryComparisonOperation) { + MySQLBinaryComparisonOperation op = (MySQLBinaryComparisonOperation) expr; + return new MySQLBinaryComparisonOperation(transformNode(op.getLeft(), SCALAR, false), + transformNode(op.getRight(), SCALAR, false), op.getOp()); + } else if (expr instanceof MySQLBinaryOperation) { + MySQLBinaryOperation op = (MySQLBinaryOperation) expr; + return new MySQLBinaryOperation(transformNode(op.getLeft(), SCALAR, false), + transformNode(op.getRight(), SCALAR, false), op.getOp()); + } else if (expr instanceof MySQLUnaryPrefixOperation) { + MySQLUnaryPrefixOperation op = (MySQLUnaryPrefixOperation) expr; + boolean childContext = op.getOp() == MySQLUnaryPrefixOperator.NOT ? BOOLEAN : SCALAR; + return new MySQLUnaryPrefixOperation(transformNode(op.getExpression(), childContext, false), op.getOp()); + } else if (expr instanceof MySQLUnaryPostfixOperation) { + // The operand is transformed value-preservingly (scalar), which is safe for IS NULL/IS TRUE/IS FALSE. + MySQLUnaryPostfixOperation op = (MySQLUnaryPostfixOperation) expr; + return new MySQLUnaryPostfixOperation(transformNode(op.getExpression(), SCALAR, false), op.getOperator(), + op.isNegated()); + } else if (expr instanceof MySQLCastOperation) { + MySQLCastOperation op = (MySQLCastOperation) expr; + return new MySQLCastOperation(transformNode(op.getExpr(), SCALAR, false), op.getType()); + } else if (expr instanceof MySQLBetweenOperation) { + MySQLBetweenOperation op = (MySQLBetweenOperation) expr; + return new MySQLBetweenOperation(transformNode(op.getExpr(), SCALAR, false), + transformNode(op.getLeft(), SCALAR, false), transformNode(op.getRight(), SCALAR, false)); + } else if (expr instanceof MySQLInOperation) { + MySQLInOperation op = (MySQLInOperation) expr; + List listElements = op.getListElements().stream().map(e -> transformNode(e, SCALAR, false)) + .collect(Collectors.toList()); + return new MySQLInOperation(transformNode(op.getExpr(), SCALAR, false), listElements, op.isTrue()); + } else if (expr instanceof MySQLComputableFunction) { + MySQLComputableFunction op = (MySQLComputableFunction) expr; + MySQLExpression[] args = op.getArguments(); + MySQLExpression[] newArgs = new MySQLExpression[args.length]; + for (int i = 0; i < args.length; i++) { + newArgs[i] = transformNode(args[i], SCALAR, false); + } + return new MySQLComputableFunction(op.getFunction(), newArgs); + } else if (expr instanceof MySQLCaseOperator) { + return descendCase((MySQLCaseOperator) expr); + } + return expr; + } + + private MySQLExpression descendCase(MySQLCaseOperator caseOp) { + MySQLExpression switchCondition = caseOp.getSwitchCondition(); + // Without a switch operand the WHEN conditions are boolean; with one they are compared against the operand. + boolean conditionContext = switchCondition == null ? BOOLEAN : SCALAR; + MySQLExpression newSwitch = switchCondition == null ? null : transformNode(switchCondition, SCALAR, false); + List conditions = caseOp.getConditions().stream() + .map(e -> transformNode(e, conditionContext, false)).collect(Collectors.toList()); + List expressions = caseOp.getExpressions().stream().map(e -> transformNode(e, SCALAR, false)) + .collect(Collectors.toList()); + MySQLExpression elseExpr = caseOp.getElseExpr() == null ? null + : transformNode(caseOp.getElseExpr(), SCALAR, false); + return new MySQLCaseOperator(newSwitch, conditions, expressions, elseExpr); + } + + @Override + protected MySQLExpression and(MySQLExpression left, MySQLExpression right) { + return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.AND); + } + + @Override + protected MySQLExpression orExpr(MySQLExpression left, MySQLExpression right) { + return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.OR); + } + + @Override + protected MySQLExpression not(MySQLExpression expr) { + return new MySQLUnaryPrefixOperation(expr, MySQLUnaryPrefixOperator.NOT); + } + + @Override + protected MySQLExpression isNull(MySQLExpression expr) { + return new MySQLUnaryPostfixOperation(expr, UnaryPostfixOperator.IS_NULL, false); + } + + @Override + protected MySQLExpression isNotNull(MySQLExpression expr) { + return new MySQLUnaryPostfixOperation(expr, UnaryPostfixOperator.IS_NULL, true); + } + + @Override + protected MySQLExpression caseWhen(MySQLExpression condition, MySQLExpression thenExpr, MySQLExpression elseExpr) { + return new MySQLCaseOperator(null, List.of(condition), List.of(thenExpr), elseExpr); + } + + @Override + protected MySQLExpression generateBooleanExpression() { + return gen.generateBooleanExpression(); + } + + @Override + protected MySQLExpression generateExpressionOfType(CastType type) { + // The MySQL expression generator is untyped, so the type of an arbitrary random expression is pinned by + // wrapping it in a CAST to the requested type. + return new MySQLCastOperation(gen.generateExpression(), type); + } + + @Override + protected CastType inferType(MySQLExpression expr) { + if (expr instanceof MySQLBinaryLogicalOperation || expr instanceof MySQLBinaryComparisonOperation + || expr instanceof MySQLUnaryPostfixOperation || expr instanceof MySQLBetweenOperation + || expr instanceof MySQLInOperation || expr instanceof MySQLExists) { + // Predicates evaluate to the boolean values 0/1, which are signed BIGINT. + return CastType.SIGNED; + } else if (expr instanceof MySQLBinaryOperation) { + // The bit operators &, | and ^ return BIGINT UNSIGNED. + return CastType.UNSIGNED; + } else if (expr instanceof MySQLCastOperation) { + return ((MySQLCastOperation) expr).getType(); + } else if (expr instanceof MySQLUnaryPrefixOperation) { + return inferUnaryPrefixType((MySQLUnaryPrefixOperation) expr); + } else if (expr instanceof MySQLConstant) { + return inferConstantType((MySQLConstant) expr); + } else if (expr instanceof MySQLColumnReference) { + return inferColumnType((MySQLColumnReference) expr); + } else if (expr instanceof MySQLComputableFunction) { + return inferFunctionType((MySQLComputableFunction) expr); + } else if (expr instanceof MySQLCaseOperator) { + return inferCaseType((MySQLCaseOperator) expr); + } + return null; + } + + private CastType inferUnaryPrefixType(MySQLUnaryPrefixOperation op) { + if (op.getOp() == MySQLUnaryPrefixOperator.NOT) { + return CastType.SIGNED; + } + CastType operandType = inferType(op.getExpression()); + if (op.getOp() == MySQLUnaryPrefixOperator.PLUS) { + return operandType; + } + if (op.getOp() == MySQLUnaryPrefixOperator.MINUS) { + if (operandType == CastType.UNSIGNED) { + return CastType.SIGNED; + } else if (operandType == CastType.FLOAT) { + return CastType.DOUBLE; + } else if (operandType != CastType.CHAR) { + return operandType; + } + } + return null; + } + + private CastType inferConstantType(MySQLConstant constant) { + if (constant instanceof MySQLConstant.MySQLIntConstant) { + return constant.isSigned() ? CastType.SIGNED : CastType.UNSIGNED; + } else if (constant instanceof MySQLConstant.MySQLTextConstant) { + return CastType.CHAR; + } else if (constant instanceof MySQLConstant.MySQLDoubleConstant) { + return CastType.DOUBLE; + } + return null; // the NULL constant has no type of its own + } + + private CastType inferColumnType(MySQLColumnReference ref) { + MySQLColumn column = ref.getColumn(); + switch (column.getType()) { + case INT: + return CastType.SIGNED; // the table generator never creates UNSIGNED INT columns + case VARCHAR: + return CastType.CHAR; + case FLOAT: + // FLOAT/DOUBLE columns are created without (M, D) while EET is active (the (M, D) form is deprecated and + // not a valid CAST target), so the plain CAST target matches the column's type. + return CastType.FLOAT; + case DOUBLE: + return CastType.DOUBLE; + case DECIMAL: + // DECIMAL columns may carry (M, D); CAST(... AS DECIMAL(M, D)) reproduces the column's exact type. The + // schema reports (M, D) even for a plain DECIMAL column (defaulting to (10, 0)). + return CastType.decimal(column.getPrecision(), column.getScale()); + default: + return null; + } + } + + private CastType inferFunctionType(MySQLComputableFunction func) { + MySQLExpression[] args = func.getArguments(); + switch (func.getFunction()) { + case BIT_COUNT: + return CastType.SIGNED; + case IF: + // The result type aggregates the types of the two value arguments (the condition does not contribute). + return commonType(args[1], args[2]); + case COALESCE: + case IFNULL: + case LEAST: + case GREATEST: + return commonType(args); + default: + return null; + } + } + + private CastType inferCaseType(MySQLCaseOperator caseOp) { + List branches = new ArrayList<>(caseOp.getExpressions()); + if (caseOp.getElseExpr() != null) { + branches.add(caseOp.getElseExpr()); + } + return commonType(branches.toArray(new MySQLExpression[0])); + } + + /** + * The common type of several result-type-determining subexpressions, or {@code null} if they do not have the same + * inferrable type (a conservative under-approximation of MySQL's aggregation rules). Two DECIMAL subexpressions + * with differing {@code (M, D)} therefore yield {@code null} rather than a guessed aggregate. + * + * @param exprs + * the result-type-determining subexpressions + * + * @return the common inferred type of {@code exprs}, or {@code null} if they do not share one + */ + private CastType commonType(MySQLExpression... exprs) { + CastType common = null; + for (MySQLExpression expr : exprs) { + CastType type = inferType(expr); + // equals (not ==) so two DECIMAL types with matching (M, D) but distinct instances compare as equal. + if (type == null || common != null && !type.equals(common)) { + return null; + } + common = type; + } + return common; + } + + @Override + protected boolean isCaseWhenApplicable(MySQLExpression expr) { + // Table references cannot be wrapped in CASE WHEN (they would cause syntax errors, see rule No. 7 of the EET + // paper); aggregates are excluded to avoid placing them in invalid contexts. + return !(expr instanceof MySQLTableReference) && !(expr instanceof MySQLAggregate); + } +} diff --git a/src/sqlancer/mysql/oracle/MySQLPivotedQuerySynthesisOracle.java b/src/sqlancer/mysql/oracle/MySQLPivotedQuerySynthesisOracle.java index 28665c328..c1fe893b6 100644 --- a/src/sqlancer/mysql/oracle/MySQLPivotedQuerySynthesisOracle.java +++ b/src/sqlancer/mysql/oracle/MySQLPivotedQuerySynthesisOracle.java @@ -38,6 +38,7 @@ public MySQLPivotedQuerySynthesisOracle(MySQLGlobalState globalState) throws SQL super(globalState); MySQLErrors.addExpressionErrors(errors); errors.add("in 'order clause'"); // e.g., Unknown column '2067708013' in 'order clause' + errors.add("in 'EXISTS subquery'"); // e.g., Unknown column '2067708013' in 'EXISTS subquery' (MySQL 8.4+) } @Override diff --git a/src/sqlancer/oceanbase/OceanBaseErrors.java b/src/sqlancer/oceanbase/OceanBaseErrors.java index c539ab129..7a5bd8a77 100644 --- a/src/sqlancer/oceanbase/OceanBaseErrors.java +++ b/src/sqlancer/oceanbase/OceanBaseErrors.java @@ -15,6 +15,7 @@ public static List getExpressionErrors() { ArrayList errors = new ArrayList<>(); errors.add("BIGINT value is out of range"); // e.g., CAST(-('-1e500') AS SIGNED) + errors.add("value is out of range"); errors.add("is not valid for CHARACTER SET"); errors.add("The observer or zone is not the master"); errors.add("Incorrect integer value"); diff --git a/src/sqlancer/oceanbase/OceanBaseOracleFactory.java b/src/sqlancer/oceanbase/OceanBaseOracleFactory.java index b7b115f37..b1ab1cb5b 100644 --- a/src/sqlancer/oceanbase/OceanBaseOracleFactory.java +++ b/src/sqlancer/oceanbase/OceanBaseOracleFactory.java @@ -17,7 +17,7 @@ public enum OceanBaseOracleFactory implements OracleFactory create(OceanBaseGlobalState globalState) throws SQLException { OceanBaseExpressionGenerator gen = new OceanBaseExpressionGenerator(globalState); ExpectedErrors expectedErrors = ExpectedErrors.newErrors().with(OceanBaseErrors.getExpressionErrors()) - .withRegex(OceanBaseErrors.getExpressionErrorsRegex()).with("value is out of range").build(); + .withRegex(OceanBaseErrors.getExpressionErrorsRegex()).build(); return new TLPWhereOracle<>(globalState, gen, expectedErrors); } diff --git a/src/sqlancer/oceanbase/OceanBaseSchema.java b/src/sqlancer/oceanbase/OceanBaseSchema.java index 7de2457ee..7b5b5954f 100644 --- a/src/sqlancer/oceanbase/OceanBaseSchema.java +++ b/src/sqlancer/oceanbase/OceanBaseSchema.java @@ -81,6 +81,7 @@ public int getPrecision() { return precision; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } @@ -195,10 +196,6 @@ public OceanBaseTable(String tableName, List columns, List c.isPrimaryKey()); - } - } public static final class OceanBaseIndex extends TableIndex { diff --git a/src/sqlancer/oceanbase/gen/OceanBaseDeleteGenerator.java b/src/sqlancer/oceanbase/gen/OceanBaseDeleteGenerator.java index ea1cb36e8..ec1aa7fc0 100644 --- a/src/sqlancer/oceanbase/gen/OceanBaseDeleteGenerator.java +++ b/src/sqlancer/oceanbase/gen/OceanBaseDeleteGenerator.java @@ -3,16 +3,15 @@ import java.util.Arrays; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.oceanbase.OceanBaseErrors; import sqlancer.oceanbase.OceanBaseGlobalState; import sqlancer.oceanbase.OceanBaseSchema.OceanBaseTable; import sqlancer.oceanbase.OceanBaseVisitor; -public class OceanBaseDeleteGenerator { +public class OceanBaseDeleteGenerator extends AbstractDeleteGenerator { - private final StringBuilder sb = new StringBuilder(); private final OceanBaseGlobalState globalState; private final Randomly r; @@ -22,14 +21,14 @@ public OceanBaseDeleteGenerator(OceanBaseGlobalState globalState) { } public static SQLQueryAdapter delete(OceanBaseGlobalState globalState) { - return new OceanBaseDeleteGenerator(globalState).generate(); + return new OceanBaseDeleteGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { OceanBaseTable randomTable = globalState.getSchema().getRandomTable(); OceanBaseExpressionGenerator gen = new OceanBaseExpressionGenerator(globalState) .setColumns(randomTable.getColumns()); - ExpectedErrors errors = new ExpectedErrors(); sb.append("DELETE"); if (Randomly.getBoolean()) { sb.append(" /*+parallel(" + r.getLong(0, 10) + ") enable_parallel_dml*/ "); @@ -37,15 +36,13 @@ private SQLQueryAdapter generate() { sb.append(" FROM "); sb.append(randomTable.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(OceanBaseVisitor.asString(gen.generateExpression())); + appendWhereClause(OceanBaseVisitor.asString(gen.generateExpression())); OceanBaseErrors.addExpressionErrors(errors); } errors.addAll(Arrays.asList("doesn't have this option", "Truncated incorrect DOUBLE value", "Truncated incorrect INTEGER value", "Truncated incorrect DECIMAL value", "Data truncated for functional index", "Incorrect value", "Out of range value for column", "Data truncation:")); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/oceanbase/gen/OceanBaseUpdateGenerator.java b/src/sqlancer/oceanbase/gen/OceanBaseUpdateGenerator.java index 950317bd2..51816e691 100644 --- a/src/sqlancer/oceanbase/gen/OceanBaseUpdateGenerator.java +++ b/src/sqlancer/oceanbase/gen/OceanBaseUpdateGenerator.java @@ -23,10 +23,11 @@ public OceanBaseUpdateGenerator(OceanBaseGlobalState globalState) { } public static SQLQueryAdapter update(OceanBaseGlobalState globalState) { - return new OceanBaseUpdateGenerator(globalState).generate(); + return new OceanBaseUpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { OceanBaseSchema.OceanBaseTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new OceanBaseExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -38,15 +39,12 @@ private SQLQueryAdapter generate() { sb.append(" SET "); updateColumns(columns); if (Randomly.getBoolean()) { - sb.append(" WHERE "); OceanBaseErrors.addExpressionErrors(errors); - sb.append(OceanBaseVisitor.asString(gen.generateExpression())); + appendWhereClause(OceanBaseVisitor.asString(gen.generateExpression())); errors.add("Data Too Long"); } errors.add("Duplicated primary key"); OceanBaseErrors.addInsertErrors(errors); - - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/postgres/PostgresProvider.java b/src/sqlancer/postgres/PostgresProvider.java index acd20a184..814013df6 100644 --- a/src/sqlancer/postgres/PostgresProvider.java +++ b/src/sqlancer/postgres/PostgresProvider.java @@ -127,6 +127,7 @@ public enum Action implements AbstractAction { LISTEN((g) -> PostgresNotifyGenerator.createListen()), // UNLISTEN((g) -> PostgresNotifyGenerator.createUnlisten()), // CREATE_SEQUENCE(PostgresSequenceGenerator::createSequence), // + EXPLAIN(PostgresExplainGenerator::create), // CREATE_VIEW(PostgresViewGenerator::create), // CREATE_TABLESPACE(PostgresTableSpaceGenerator::generate); @@ -193,7 +194,7 @@ protected static int mapActions(PostgresGlobalState globalState, Action a) { nrPerformed = r.getInteger(0, 2); break; case CREATE_TABLESPACE: - nrPerformed = r.getInteger(0, 2); + nrPerformed = globalState.getDbmsSpecificOptions().isTestTablespaces() ? r.getInteger(0, 2) : 0; break; case UPDATE: nrPerformed = r.getInteger(0, 10); @@ -201,6 +202,9 @@ protected static int mapActions(PostgresGlobalState globalState, Action a) { case INSERT: nrPerformed = r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); break; + case EXPLAIN: + nrPerformed = r.getInteger(0, 1); + break; default: throw new AssertionError(a); } diff --git a/src/sqlancer/postgres/PostgresSchema.java b/src/sqlancer/postgres/PostgresSchema.java index c99c8648e..82937557c 100644 --- a/src/sqlancer/postgres/PostgresSchema.java +++ b/src/sqlancer/postgres/PostgresSchema.java @@ -125,6 +125,12 @@ public static PostgresDataType getColumnType(String typeString) { case "character varying": case "name": case "regclass": + case "regnamespace": + case "regrole": + case "regtype": + case "regproc": + case "regprocedure": + case "regoper": return PostgresDataType.TEXT; case "numeric": return PostgresDataType.DECIMAL; @@ -164,6 +170,7 @@ public enum TableType { private final TableType tableType; private final List statistics; private final boolean isInsertable; + private final boolean isPartitioned; public PostgresTable(String tableName, List columns, List indexes, TableType tableType, List statistics, boolean isView, boolean isInsertable) { @@ -171,6 +178,18 @@ public PostgresTable(String tableName, List columns, List columns, List indexes, + TableType tableType, List statistics, boolean isView, boolean isInsertable, + boolean isPartitioned) { + super(tableName, columns, indexes, isView); + this.statistics = statistics; + this.isInsertable = isInsertable; + this.tableType = tableType; + this.isPartitioned = isPartitioned; } public List getStatistics() { @@ -185,6 +204,10 @@ public boolean isInsertable() { return isInsertable; } + public boolean isPartitioned() { + return isPartitioned; + } + } public static final class PostgresStatisticsObject { @@ -225,22 +248,23 @@ public static PostgresSchema fromConnection(SQLConnection con, String databaseNa List databaseTables = new ArrayList<>(); try (Statement s = con.createStatement()) { try (ResultSet rs = s.executeQuery( - "SELECT table_name, table_schema, table_type, is_insertable_into FROM information_schema.tables WHERE table_schema='public' OR table_schema LIKE 'pg_temp_%' ORDER BY table_name;")) { + "SELECT t.table_name, t.table_schema, t.table_type, t.is_insertable_into, c.relkind FROM information_schema.tables t JOIN pg_class c ON c.relname = t.table_name JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = t.table_schema WHERE t.table_schema='public' OR t.table_schema LIKE 'pg_temp_%' ORDER BY t.table_name;")) { while (rs.next()) { String tableName = rs.getString("table_name"); String tableTypeSchema = rs.getString("table_schema"); boolean isInsertable = rs.getBoolean("is_insertable_into"); + boolean isPartitioned = "p".equals(rs.getString("relkind")); // TODO: also check insertable // TODO: insert into view? - boolean isView = tableName.startsWith("v"); // tableTypeStr.contains("VIEW") || - // tableTypeStr.contains("LOCAL TEMPORARY") && - // !isInsertable; + boolean isView = matchesViewName(tableName); // tableTypeStr.contains("VIEW") || + // tableTypeStr.contains("LOCAL TEMPORARY") && + // !isInsertable; PostgresTable.TableType tableType = getTableType(tableTypeSchema); List databaseColumns = getTableColumns(con, tableName); List indexes = getIndexes(con, tableName); List statistics = getStatistics(con); PostgresTable t = new PostgresTable(tableName, databaseColumns, indexes, tableType, statistics, - isView, isInsertable); + isView, isInsertable, isPartitioned); for (PostgresColumn c : databaseColumns) { c.setTable(t); } diff --git a/src/sqlancer/postgres/ast/PostgresBinaryComparisonOperation.java b/src/sqlancer/postgres/ast/PostgresBinaryComparisonOperation.java index 95efe5b72..b77060dfd 100644 --- a/src/sqlancer/postgres/ast/PostgresBinaryComparisonOperation.java +++ b/src/sqlancer/postgres/ast/PostgresBinaryComparisonOperation.java @@ -1,5 +1,6 @@ package sqlancer.postgres.ast; +import sqlancer.IgnoreMeException; import sqlancer.Randomly; import sqlancer.common.ast.BinaryOperatorNode; import sqlancer.common.ast.BinaryOperatorNode.Operator; @@ -126,7 +127,7 @@ public PostgresConstant getExpectedValue() { PostgresConstant leftExpectedValue = getLeft().getExpectedValue(); PostgresConstant rightExpectedValue = getRight().getExpectedValue(); if (leftExpectedValue == null || rightExpectedValue == null) { - return null; + throw new IgnoreMeException(); } return getOp().getExpectedValue(leftExpectedValue, rightExpectedValue); } diff --git a/src/sqlancer/postgres/gen/PostgresAlterTableGenerator.java b/src/sqlancer/postgres/gen/PostgresAlterTableGenerator.java index 6e0c436ee..69b509f60 100644 --- a/src/sqlancer/postgres/gen/PostgresAlterTableGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresAlterTableGenerator.java @@ -127,6 +127,9 @@ public List getActions(ExpectedErrors errors) { if (!randomTable.hasIndexes()) { action.remove(Action.ADD_TABLE_CONSTRAINT_USING_INDEX); } + if (randomTable.isPartitioned()) { + action.remove(Action.SET_LOGGED_UNLOGGED); + } if (action.isEmpty()) { throw new IgnoreMeException(); } @@ -235,6 +238,8 @@ public SQLQueryAdapter generate() { errors.add("is in a primary key"); errors.add("is an identity column"); errors.add("is in index used as replica identity"); + // PG18 update: otherwise we need to encode contraint inheritance info in PostgreColumn + errors.add("cannot drop inherited constraint"); } break; case ALTER_COLUMN_SET_STATISTICS: @@ -299,6 +304,7 @@ public SQLQueryAdapter generate() { errors.add("multiple primary keys for table"); errors.add("could not create unique index"); errors.add("contains null values"); + errors.add("is not marked NOT NULL"); errors.add("cannot cast type"); errors.add("unsupported PRIMARY KEY constraint with partition key definition"); errors.add("unsupported UNIQUE constraint with partition key definition"); @@ -337,6 +343,7 @@ public SQLQueryAdapter generate() { errors.add("appears twice in unique constraint"); errors.add("appears twice in primary key constraint"); errors.add("contains null values"); + errors.add("is not marked NOT NULL"); errors.add("insufficient columns in PRIMARY KEY constraint definition"); errors.add("which is part of the partition key"); break; diff --git a/src/sqlancer/postgres/gen/PostgresCommon.java b/src/sqlancer/postgres/gen/PostgresCommon.java index 63bc885cb..180b4e449 100644 --- a/src/sqlancer/postgres/gen/PostgresCommon.java +++ b/src/sqlancer/postgres/gen/PostgresCommon.java @@ -48,6 +48,7 @@ public static List getCommonTableErrors() { errors.add("is not commutative"); // exclude errors.add("operator requires run-time type coercion"); // exclude + errors.add("partitioned tables cannot be unlogged"); return errors; } @@ -59,6 +60,8 @@ public static void addCommonTableErrors(ExpectedErrors errors) { public static List getCommonExpressionErrors() { ArrayList errors = new ArrayList<>(); + errors.add("for encoding \"SQL_ASCII\" does not exist"); + errors.add("invalid byte sequence for encoding"); errors.add("You might need to add explicit type casts"); errors.add("invalid regular expression"); errors.add("could not determine which collation to use"); @@ -119,6 +122,8 @@ private static List getToCharFunctionErrors() { errors.add("cannot use \"S\" and \"PL\" together"); errors.add("cannot use \"PR\" and \"S\"/\"PL\"/\"MI\"/\"SG\" together"); errors.add("is not a number"); + errors.add("\"EEEE\" must be the last pattern used"); + errors.add("is incompatible with other formats"); return errors; } diff --git a/src/sqlancer/postgres/gen/PostgresDeleteGenerator.java b/src/sqlancer/postgres/gen/PostgresDeleteGenerator.java index f827331c0..3250b86c8 100644 --- a/src/sqlancer/postgres/gen/PostgresDeleteGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresDeleteGenerator.java @@ -1,38 +1,38 @@ package sqlancer.postgres.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.postgres.PostgresGlobalState; import sqlancer.postgres.PostgresSchema.PostgresDataType; import sqlancer.postgres.PostgresSchema.PostgresTable; import sqlancer.postgres.PostgresVisitor; -public final class PostgresDeleteGenerator { +public final class PostgresDeleteGenerator extends AbstractDeleteGenerator { - private PostgresDeleteGenerator() { + private final PostgresGlobalState globalState; + + private PostgresDeleteGenerator(PostgresGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter create(PostgresGlobalState globalState) { + return new PostgresDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { PostgresTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - ExpectedErrors errors = new ExpectedErrors(); errors.add("violates foreign key constraint"); errors.add("violates not-null constraint"); errors.add("could not determine which collation to use for string comparison"); - StringBuilder sb = new StringBuilder("DELETE FROM"); - if (Randomly.getBoolean()) { - sb.append(" ONLY"); - } - sb.append(" "); - sb.append(table.getName()); + appendDeleteFromTable(table.getName(), Randomly.getBoolean()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(PostgresVisitor.asString(PostgresExpressionGenerator.generateExpression(globalState, + appendWhereClause(PostgresVisitor.asString(PostgresExpressionGenerator.generateExpression(globalState, table.getColumns(), PostgresDataType.BOOLEAN))); } if (Randomly.getBoolean()) { - sb.append(" RETURNING "); - sb.append(PostgresVisitor + appendReturningClause(PostgresVisitor .asString(PostgresExpressionGenerator.generateExpression(globalState, table.getColumns()))); } PostgresCommon.addCommonExpressionErrors(errors); @@ -40,7 +40,6 @@ public static SQLQueryAdapter create(PostgresGlobalState globalState) { errors.add("cannot cast"); errors.add("invalid input syntax for"); errors.add("division by zero"); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/postgres/gen/PostgresExplainGenerator.java b/src/sqlancer/postgres/gen/PostgresExplainGenerator.java index d3039394b..e4359e5aa 100644 --- a/src/sqlancer/postgres/gen/PostgresExplainGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresExplainGenerator.java @@ -1,16 +1,82 @@ package sqlancer.postgres.gen; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.postgres.PostgresGlobalState; +import sqlancer.postgres.PostgresSchema; +import sqlancer.postgres.PostgresSchema.PostgresDataType; +import sqlancer.postgres.PostgresSchema.PostgresTables; +import sqlancer.postgres.ast.PostgresSelect; + public final class PostgresExplainGenerator { private PostgresExplainGenerator() { } - public static String explain(String selectStr) throws Exception { + public static String explain(String selectStr) { StringBuilder sb = new StringBuilder(); sb.append("EXPLAIN (FORMAT JSON) "); sb.append(selectStr); return sb.toString(); } + public static String explainGeneral(String selectStr) { + StringBuilder sb = new StringBuilder(); + sb.append("EXPLAIN "); + + List options = new ArrayList<>(); + boolean analyze = Randomly.getBoolean(); + boolean genericPlan = !analyze && Randomly.getBoolean(); + if (analyze) { + options.add("ANALYZE"); + } + if (genericPlan) { + options.add("GENERIC_PLAN"); + } + if (Randomly.getBoolean()) { + options.add("FORMAT " + Randomly.fromOptions("TEXT", "XML", "JSON", "YAML")); + } + if (Randomly.getBoolean()) { + options.add("VERBOSE"); + } + if (Randomly.getBoolean()) { + options.add("COSTS"); + } + if (analyze && Randomly.getBoolean()) { + options.add("BUFFERS"); + } + if (analyze && Randomly.getBoolean()) { + options.add("TIMING"); + } + if (Randomly.getBoolean()) { + options.add("SUMMARY"); + } + if (!options.isEmpty()) { + sb.append("("); + sb.append(String.join(", ", options)); + sb.append(") "); + } + + sb.append(selectStr); + return sb.toString(); + } + + public static SQLQueryAdapter create(PostgresGlobalState globalState) throws Exception { + PostgresSchema.PostgresTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); + PostgresExpressionGenerator gen = new PostgresExpressionGenerator(globalState); + gen.setTablesAndColumns(new PostgresTables(Arrays.asList(table))); + PostgresSelect select = gen.generateSelect(); + select.setFromList(gen.getTableRefs()); + select.setFetchColumns(gen.generateFetchColumns(false)); + if (Randomly.getBoolean()) { + select.setWhereClause(gen.generateExpression(PostgresDataType.BOOLEAN)); + } + return new SQLQueryAdapter(explainGeneral(select.asString())); + } + } diff --git a/src/sqlancer/postgres/gen/PostgresIndexGenerator.java b/src/sqlancer/postgres/gen/PostgresIndexGenerator.java index 4cb2b8e3a..c852684a9 100644 --- a/src/sqlancer/postgres/gen/PostgresIndexGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresIndexGenerator.java @@ -5,7 +5,7 @@ import sqlancer.Randomly; import sqlancer.common.DBMSCommon; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.postgres.PostgresGlobalState; import sqlancer.postgres.PostgresSchema.PostgresColumn; @@ -15,9 +15,12 @@ import sqlancer.postgres.PostgresVisitor; import sqlancer.postgres.ast.PostgresExpression; -public final class PostgresIndexGenerator { +public class PostgresIndexGenerator extends AbstractIndexGenerator { - private PostgresIndexGenerator() { + private final PostgresGlobalState globalState; + + public PostgresIndexGenerator(PostgresGlobalState globalState) { + this.globalState = globalState; } public enum IndexType { @@ -25,13 +28,12 @@ public enum IndexType { } public static SQLQueryAdapter generate(PostgresGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); - sb.append("CREATE"); - if (Randomly.getBoolean()) { - sb.append(" UNIQUE"); - } - sb.append(" INDEX "); + return new PostgresIndexGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { + appendCreateIndex(Randomly.getBoolean()); /* * Commented out as a workaround for https://www.postgresql.org/message-id/CA%2Bu7OA4XYhc- * qyCgJqwwgMGZDWAyeH821oa5oMzm_HEifZ4BeA%40mail.gmail.com @@ -105,10 +107,9 @@ public static SQLQueryAdapter generate(PostgresGlobalState globalState) { sb.append(")"); } if (Randomly.getBoolean()) { - sb.append(" WHERE "); PostgresExpression expr = new PostgresExpressionGenerator(globalState).setColumns(randomTable.getColumns()) .setGlobalState(globalState).generateExpression(PostgresDataType.BOOLEAN); - sb.append(PostgresVisitor.asString(expr)); + appendWhereClause(PostgresVisitor.asString(expr)); } errors.add("already contains data"); // CONCURRENT INDEX failed errors.add("You might need to add explicit type casts"); @@ -136,7 +137,6 @@ public static SQLQueryAdapter generate(PostgresGlobalState globalState) { errors.add("result of range difference would not be contiguous"); errors.add("which is part of the partition key"); PostgresCommon.addCommonExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } private static String getNewIndexName(PostgresTable randomTable) { diff --git a/src/sqlancer/postgres/gen/PostgresReindexGenerator.java b/src/sqlancer/postgres/gen/PostgresReindexGenerator.java index 9bb5ec5cd..d22ffe53e 100644 --- a/src/sqlancer/postgres/gen/PostgresReindexGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresReindexGenerator.java @@ -1,7 +1,6 @@ package sqlancer.postgres.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.IgnoreMeException; import sqlancer.Randomly; @@ -39,7 +38,7 @@ public static SQLQueryAdapter create(PostgresGlobalState globalState) { if (indexes.isEmpty()) { throw new IgnoreMeException(); } - sb.append(indexes.stream().map(i -> i.getIndexName()).collect(Collectors.joining())); + sb.append(Randomly.fromList(indexes).getIndexName()); break; case TABLE: sb.append("TABLE "); @@ -59,7 +58,6 @@ public static SQLQueryAdapter create(PostgresGlobalState globalState) { throw new AssertionError(scope); } errors.add("already contains data"); // FIXME bug report - errors.add("does not exist"); // internal index errors.add("REINDEX is not yet implemented for partitioned indexes"); return new SQLQueryAdapter(sb.toString(), errors); } diff --git a/src/sqlancer/postgres/gen/PostgresTableGenerator.java b/src/sqlancer/postgres/gen/PostgresTableGenerator.java index 29ccfcf2c..9a7bfb032 100644 --- a/src/sqlancer/postgres/gen/PostgresTableGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresTableGenerator.java @@ -206,7 +206,7 @@ private void generateUsing() { sb.append(globalState.getRandomTableAccessMethod()); } - private void generateInherits() { + protected void generateInherits() { if (Randomly.getBoolean() && !newSchema.getDatabaseTablesWithoutViews().isEmpty()) { sb.append(" INHERITS("); sb.append(newSchema.getDatabaseTablesRandomSubsetNotEmpty().stream().map(t -> t.getName()) diff --git a/src/sqlancer/postgres/gen/PostgresTableSpaceGenerator.java b/src/sqlancer/postgres/gen/PostgresTableSpaceGenerator.java index 3890d5160..1be99ca61 100644 --- a/src/sqlancer/postgres/gen/PostgresTableSpaceGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresTableSpaceGenerator.java @@ -1,5 +1,6 @@ package sqlancer.postgres.gen; +import sqlancer.IgnoreMeException; import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.postgres.PostgresGlobalState; @@ -19,10 +20,12 @@ public PostgresTableSpaceGenerator(PostgresGlobalState globalState) { } public static SQLQueryAdapter generate(PostgresGlobalState globalState) { - // Skip tablespace generation if the option is disabled + // PostgresProvider.mapActions does not schedule this action when the option is disabled, but QPG + // selects actions by index without consulting the schedule, so the generator has to report that + // it has nothing to generate. PostgresOptions options = globalState.getDbmsSpecificOptions(); if (!options.isTestTablespaces()) { - return null; + throw new IgnoreMeException(); } return new PostgresTableSpaceGenerator(globalState).generateTableSpace(); } diff --git a/src/sqlancer/postgres/gen/PostgresUpdateGenerator.java b/src/sqlancer/postgres/gen/PostgresUpdateGenerator.java index 7ce7fe882..92257a95a 100644 --- a/src/sqlancer/postgres/gen/PostgresUpdateGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresUpdateGenerator.java @@ -20,6 +20,7 @@ public final class PostgresUpdateGenerator extends AbstractUpdateGenerator t.isInsertable()); List columns = randomTable.getRandomNonEmptyColumnSubset(); sb.append("UPDATE "); @@ -50,13 +52,10 @@ private SQLQueryAdapter generate() { errors.add("but expression is of type"); PostgresCommon.addCommonExpressionErrors(errors); if (!Randomly.getBooleanWithSmallProbability()) { - sb.append(" WHERE "); PostgresExpression where = PostgresExpressionGenerator.generateExpression(globalState, randomTable.getColumns(), PostgresDataType.BOOLEAN); - sb.append(PostgresVisitor.asString(where)); + appendWhereClause(PostgresVisitor.asString(where)); } - - return new SQLQueryAdapter(sb.toString(), errors, true); } @Override diff --git a/src/sqlancer/postgres/gen/PostgresViewGenerator.java b/src/sqlancer/postgres/gen/PostgresViewGenerator.java index b0a2a8b9d..10992ece6 100644 --- a/src/sqlancer/postgres/gen/PostgresViewGenerator.java +++ b/src/sqlancer/postgres/gen/PostgresViewGenerator.java @@ -35,19 +35,11 @@ public static SQLQueryAdapter create(PostgresGlobalState globalState) { materialized = false; } sb.append(" VIEW "); - int i = 0; - String[] name = new String[1]; - while (true) { - name[0] = "v" + i++; - if (globalState.getSchema().getDatabaseTables().stream() - .noneMatch(tab -> tab.getName().contentEquals(name[0]))) { - break; - } - } - sb.append(name[0]); + String name = globalState.getSchema().getFreeViewName(); + sb.append(name); sb.append("("); int nrColumns = Randomly.smallNumber() + 1; - for (i = 0; i < nrColumns; i++) { + for (int i = 0; i < nrColumns; i++) { if (i != 0) { sb.append(", "); } diff --git a/src/sqlancer/presto/PrestoBugs.java b/src/sqlancer/presto/PrestoBugs.java index f5e888df1..b0eb3fe57 100644 --- a/src/sqlancer/presto/PrestoBugs.java +++ b/src/sqlancer/presto/PrestoBugs.java @@ -8,6 +8,12 @@ public final class PrestoBugs { // https://github.com/prestodb/presto/issues/23613 public static boolean bug23613 = true; + // https://github.com/prestodb/presto/issues/27608 + public static boolean bugVerifyError = true; + + // https://github.com/prestodb/presto/issues/27609 + public static boolean bugCompilerFailed = true; + private PrestoBugs() { } diff --git a/src/sqlancer/presto/PrestoErrors.java b/src/sqlancer/presto/PrestoErrors.java index 2296223d6..dd2931976 100644 --- a/src/sqlancer/presto/PrestoErrors.java +++ b/src/sqlancer/presto/PrestoErrors.java @@ -47,6 +47,13 @@ public static List getExpressionErrors() { } errors.add("Cannot cast java.lang.String to java.util.List"); errors.add("Unexpected subquery expression in logical plan"); + if (PrestoBugs.bugVerifyError) { + errors.add("VerifyError"); + } + if (PrestoBugs.bugCompilerFailed) { + errors.add("Compiler failed"); + errors.add("Error processing class definition"); + } // 9223372036854775808 errors.add("Invalid numeric literal"); diff --git a/src/sqlancer/presto/PrestoSchema.java b/src/sqlancer/presto/PrestoSchema.java index 112da7ef2..2e668969d 100644 --- a/src/sqlancer/presto/PrestoSchema.java +++ b/src/sqlancer/presto/PrestoSchema.java @@ -27,7 +27,7 @@ public static PrestoSchema fromConnection(SQLConnection con, String databaseName List tableNames = getTableNames(con); for (String tableName : tableNames) { List databaseColumns = getTableColumns(con, databaseName, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); PrestoTable t = new PrestoTable(tableName, databaseColumns, isView); for (PrestoColumn c : databaseColumns) { c.setTable(t); @@ -453,6 +453,7 @@ public PrestoColumn(String name, PrestoCompositeDataType columnType, boolean isP this.isNullable = isNullable; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } diff --git a/src/sqlancer/presto/gen/PrestoDeleteGenerator.java b/src/sqlancer/presto/gen/PrestoDeleteGenerator.java index 9f869c241..4d5724992 100644 --- a/src/sqlancer/presto/gen/PrestoDeleteGenerator.java +++ b/src/sqlancer/presto/gen/PrestoDeleteGenerator.java @@ -1,7 +1,7 @@ package sqlancer.presto.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.presto.PrestoErrors; import sqlancer.presto.PrestoGlobalState; @@ -9,24 +9,29 @@ import sqlancer.presto.PrestoSchema.PrestoTable; import sqlancer.presto.PrestoToStringVisitor; -public final class PrestoDeleteGenerator { +public final class PrestoDeleteGenerator extends AbstractDeleteGenerator { - private PrestoDeleteGenerator() { + private final PrestoGlobalState globalState; + + private PrestoDeleteGenerator(PrestoGlobalState globalState) { + this.globalState = globalState; + this.canonicalizeString = false; } public static SQLQueryAdapter generate(PrestoGlobalState globalState) { - StringBuilder sb = new StringBuilder("DELETE FROM "); - ExpectedErrors errors = new ExpectedErrors(); + return new PrestoDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { PrestoTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - sb.append(table.getName()); + appendDeleteFromTable(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(PrestoToStringVisitor + appendWhereClause(PrestoToStringVisitor .asString(new PrestoTypedExpressionGenerator(globalState).setColumns(table.getColumns()) .generateExpression(PrestoSchema.PrestoCompositeDataType.getRandomWithoutNull()))); } PrestoErrors.addExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors, false, false); } } diff --git a/src/sqlancer/presto/gen/PrestoIndexGenerator.java b/src/sqlancer/presto/gen/PrestoIndexGenerator.java index c76283ee4..2d7b75cf8 100644 --- a/src/sqlancer/presto/gen/PrestoIndexGenerator.java +++ b/src/sqlancer/presto/gen/PrestoIndexGenerator.java @@ -3,7 +3,7 @@ import java.util.List; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.presto.PrestoGlobalState; import sqlancer.presto.PrestoSchema; @@ -12,20 +12,27 @@ import sqlancer.presto.PrestoToStringVisitor; import sqlancer.presto.ast.PrestoExpression; -public final class PrestoIndexGenerator { +public class PrestoIndexGenerator extends AbstractIndexGenerator { - private PrestoIndexGenerator() { + private final PrestoGlobalState globalState; + + public PrestoIndexGenerator(PrestoGlobalState globalState) { + this.globalState = globalState; + this.canAffectSchema = true; + this.canonicalizeString = false; } public static SQLQueryAdapter getQuery(PrestoGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); - sb.append("CREATE "); - if (Randomly.getBoolean()) { + return new PrestoIndexGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { + boolean unique = Randomly.getBoolean(); + if (unique) { errors.add("Cant create unique index, table contains duplicate data on indexed column(s)"); - sb.append("UNIQUE "); } - sb.append("INDEX "); + appendCreateIndex(unique); sb.append(Randomly.fromOptions("i0", "i1", "i2", "i3", "i4")); // cannot query this information sb.append(" ON "); PrestoTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); @@ -44,13 +51,11 @@ public static SQLQueryAdapter getQuery(PrestoGlobalState globalState) { } sb.append(")"); if (Randomly.getBoolean()) { - sb.append(" WHERE "); PrestoExpression expr = new PrestoTypedExpressionGenerator(globalState).setColumns(table.getColumns()) .generateExpression(PrestoSchema.PrestoCompositeDataType.getRandomWithoutNull()); - sb.append(PrestoToStringVisitor.asString(expr)); + appendWhereClause(PrestoToStringVisitor.asString(expr)); } errors.add("already exists!"); - return new SQLQueryAdapter(sb.toString(), errors, true, false); } } diff --git a/src/sqlancer/presto/gen/PrestoInsertGenerator.java b/src/sqlancer/presto/gen/PrestoInsertGenerator.java index 072a22ae0..15d5f3543 100644 --- a/src/sqlancer/presto/gen/PrestoInsertGenerator.java +++ b/src/sqlancer/presto/gen/PrestoInsertGenerator.java @@ -1,10 +1,8 @@ package sqlancer.presto.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.presto.PrestoErrors; import sqlancer.presto.PrestoGlobalState; @@ -19,25 +17,19 @@ public class PrestoInsertGenerator extends AbstractInsertGenerator public PrestoInsertGenerator(PrestoGlobalState globalState) { this.globalState = globalState; + this.canonicalizeString = false; } public static SQLQueryAdapter getQuery(PrestoGlobalState globalState) { - return new PrestoInsertGenerator(globalState).generate(); + return new PrestoInsertGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { - sb.append("INSERT INTO "); + @Override + public void buildStatement() { PrestoTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); - sb.append(table.getName()); - sb.append("("); - sb.append(columns.stream().map(c -> c.getName()).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); - ExpectedErrors errors = new ExpectedErrors(); + buildInsertInto(table.getName(), columns); PrestoErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors, false, false); } @Override diff --git a/src/sqlancer/presto/gen/PrestoTableGenerator.java b/src/sqlancer/presto/gen/PrestoTableGenerator.java index 1d7df2ee6..49e1346ad 100644 --- a/src/sqlancer/presto/gen/PrestoTableGenerator.java +++ b/src/sqlancer/presto/gen/PrestoTableGenerator.java @@ -4,13 +4,35 @@ import java.util.List; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractTableGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.presto.PrestoGlobalState; import sqlancer.presto.PrestoSchema.PrestoColumn; import sqlancer.presto.PrestoSchema.PrestoCompositeDataType; -public class PrestoTableGenerator { +public class PrestoTableGenerator extends AbstractTableGenerator { + + private PrestoGlobalState globalState; + + public PrestoTableGenerator() { + this.canAffectSchema = true; + this.canonicalizeString = false; + } + + public SQLQueryAdapter getQuery(PrestoGlobalState globalState) { + this.globalState = globalState; + return getStatement(); + } + + @Override + public void buildStatement() { + String catalog = globalState.getDbmsSpecificOptions().catalog; + String schema = globalState.getDatabaseName(); + String tableName = globalState.getSchema().getFreeTableName(); + String qualifiedName = catalog + "." + schema + "." + tableName; + appendCreateTable(qualifiedName); + appendColumnDefinitions(getNewColumns()); + } private static List getNewColumns() { List columns = new ArrayList<>(); @@ -22,48 +44,4 @@ private static List getNewColumns() { return columns; } - public SQLQueryAdapter getQuery(PrestoGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); - String tableName = globalState.getSchema().getFreeTableName(); - sb.append("CREATE TABLE "); - String catalog = globalState.getDbmsSpecificOptions().catalog; - String schema = globalState.getDatabaseName(); - - sb.append(catalog).append("."); - sb.append(schema).append("."); - - sb.append(tableName); - sb.append("("); - List columns = getNewColumns(); - // TypedExpressionGenerator, PrestoColumn, PrestoCompositeDataType> - // typedExpressionGenerator = new PrestoTypedExpressionGenerator(globalState).setColumns(columns); - for (int i = 0; i < columns.size(); i++) { - if (i != 0) { - sb.append(", "); - } - PrestoColumn column = columns.get(i); - sb.append(column.getName()); - sb.append(" "); - sb.append(column.getType()); - // if (globalState.getDbmsSpecificOptions().testIndexes && Randomly.getBooleanWithRatherLowProbability()) { - // sb.append(" UNIQUE"); - // } - // if (globalState.getDbmsSpecificOptions().testNotNullConstraints - // && Randomly.getBooleanWithRatherLowProbability()) { - // sb.append(" NOT NULL"); - // } - } - // if (globalState.getDbmsSpecificOptions().testIndexes && Randomly.getBoolean()) { - // errors.add("Invalid type for index"); - // List primaryKeyColumns = Randomly.nonEmptySubset(columns); - // sb.append(", PRIMARY KEY("); - // sb.append(primaryKeyColumns.stream().map(c -> c.getName()).collect(Collectors.joining(", "))); - // sb.append(")"); - // } - sb.append(")"); - - return new SQLQueryAdapter(sb.toString(), errors, true, false); - } - } diff --git a/src/sqlancer/presto/gen/PrestoUpdateGenerator.java b/src/sqlancer/presto/gen/PrestoUpdateGenerator.java index a8afcc578..3c197f0b9 100644 --- a/src/sqlancer/presto/gen/PrestoUpdateGenerator.java +++ b/src/sqlancer/presto/gen/PrestoUpdateGenerator.java @@ -19,13 +19,15 @@ public final class PrestoUpdateGenerator extends AbstractUpdateGenerator !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new PrestoTypedExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -34,7 +36,6 @@ private SQLQueryAdapter generate() { sb.append(" SET "); updateColumns(columns); PrestoErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors, false, false); } @Override diff --git a/src/sqlancer/questdb/QuestDBSchema.java b/src/sqlancer/questdb/QuestDBSchema.java index 8253ec82c..55ee01aab 100644 --- a/src/sqlancer/questdb/QuestDBSchema.java +++ b/src/sqlancer/questdb/QuestDBSchema.java @@ -268,7 +268,7 @@ public static QuestDBSchema fromConnection(SQLConnection con, String databaseNam continue; // TODO: unexpected? } List databaseColumns = getTableColumns(con, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); QuestDBTable t = new QuestDBTable(tableName, databaseColumns, isView); for (QuestDBColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/questdb/gen/QuestDBInsertGenerator.java b/src/sqlancer/questdb/gen/QuestDBInsertGenerator.java index 754a0d2e5..41ba72406 100644 --- a/src/sqlancer/questdb/gen/QuestDBInsertGenerator.java +++ b/src/sqlancer/questdb/gen/QuestDBInsertGenerator.java @@ -1,12 +1,9 @@ package sqlancer.questdb.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; -import sqlancer.common.schema.AbstractTableColumn; import sqlancer.questdb.QuestDBErrors; import sqlancer.questdb.QuestDBProvider.QuestDBGlobalState; import sqlancer.questdb.QuestDBSchema.QuestDBColumn; @@ -17,28 +14,20 @@ public class QuestDBInsertGenerator extends AbstractInsertGenerator columns = table.getRandomNonEmptyColumnSubset(); - sb.append(table.getName()); - sb.append("("); - sb.append(columns.stream().map(AbstractTableColumn::getName).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); + buildInsertInto(table.getName(), columns); QuestDBErrors.addInsertErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); - } - - public static SQLQueryAdapter getQuery(QuestDBGlobalState globalState) { - return new QuestDBInsertGenerator(globalState).generate(); } @Override diff --git a/src/sqlancer/questdb/gen/QuestDBTableGenerator.java b/src/sqlancer/questdb/gen/QuestDBTableGenerator.java index a308b8d10..d4b17e76b 100644 --- a/src/sqlancer/questdb/gen/QuestDBTableGenerator.java +++ b/src/sqlancer/questdb/gen/QuestDBTableGenerator.java @@ -5,40 +5,37 @@ import javax.annotation.Nullable; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractTableGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.questdb.QuestDBProvider.QuestDBGlobalState; import sqlancer.questdb.QuestDBSchema.QuestDBColumn; import sqlancer.questdb.QuestDBSchema.QuestDBCompositeDataType; -public class QuestDBTableGenerator { +public class QuestDBTableGenerator extends AbstractTableGenerator { + + private QuestDBGlobalState globalState; + private String tableName; + + public QuestDBTableGenerator() { + this.canAffectSchema = true; + } public SQLQueryAdapter getQuery(QuestDBGlobalState globalState, @Nullable String tableName) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); + this.globalState = globalState; + this.tableName = tableName; + return getStatement(); + } + + @Override + public void buildStatement() { String name = tableName; - if (tableName == null) { + if (name == null) { name = globalState.getSchema().getFreeTableName(); } - sb.append("CREATE TABLE "); - if (Randomly.getBoolean()) { - sb.append("IF NOT EXISTS "); - } - sb.append(name); - sb.append("("); - List columns = getNewColumns(); - for (int i = 0; i < columns.size(); i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(columns.get(i).getName()); - sb.append(" "); - sb.append(columns.get(i).getType()); - } - sb.append(")"); + appendCreateTable(name, Randomly.getBoolean()); + appendColumnDefinitions(getNewColumns()); sb.append(";"); errors.add("table already exists"); - return new SQLQueryAdapter(sb.toString(), errors, true); } private static List getNewColumns() { diff --git a/src/sqlancer/spark/SparkErrors.java b/src/sqlancer/spark/SparkErrors.java new file mode 100644 index 000000000..a3a96061f --- /dev/null +++ b/src/sqlancer/spark/SparkErrors.java @@ -0,0 +1,65 @@ +package sqlancer.spark; + +import java.util.ArrayList; +import java.util.List; + +import sqlancer.common.query.ExpectedErrors; + +public final class SparkErrors { + + private SparkErrors() { + } + + public static List getExpressionErrors() { + ArrayList errors = new ArrayList<>(); + + errors.add("cannot resolve"); + errors.add("AnalysisException"); + errors.add("data type mismatch"); + errors.add("undefined function"); + errors.add("mismatched input"); + errors.add("due to data type mismatch"); + + // --- Invalid Literals + errors.add("The value of the typed literal"); + + errors.add("DATATYPE_MISMATCH"); + errors.add("cannot be cast to"); + + errors.add("Overflow"); + errors.add("Divide by zero"); // Common if spark.sql.ansi.enabled is true + errors.add("division by zero"); + + // --- Group By / Aggregation errors --- + errors.add("grouping expressions"); + errors.add("expression is neither present in the group by"); + errors.add("is not a valid grouping expression"); + errors.add("is not contained in either an aggregate function or the GROUP BY clause"); + + return errors; + } + + public static void addExpressionErrors(ExpectedErrors errors) { + errors.addAll(getExpressionErrors()); + } + + public static List getInsertErrors() { + ArrayList errors = new ArrayList<>(); + + errors.add("not enough data columns"); + errors.add("cannot write to"); + errors.add("incompatible types"); + errors.add("too many data columns"); + errors.add("cannot be cast to"); + errors.add("Error running query"); + errors.add("The value of the typed literal"); + errors.add("Cannot safely cast"); // Found in logs: Decimal -> Date + errors.add("AnalysisException"); // Spark throws this for almost all insert failures + + return errors; + } + + public static void addInsertErrors(ExpectedErrors errors) { + errors.addAll(getInsertErrors()); + } +} diff --git a/src/sqlancer/spark/SparkGlobalState.java b/src/sqlancer/spark/SparkGlobalState.java new file mode 100644 index 000000000..d78c737e5 --- /dev/null +++ b/src/sqlancer/spark/SparkGlobalState.java @@ -0,0 +1,11 @@ +package sqlancer.spark; + +import sqlancer.SQLGlobalState; + +public class SparkGlobalState extends SQLGlobalState { + + @Override + protected SparkSchema readSchema() throws Exception { + return SparkSchema.fromConnection(getConnection(), getDatabaseName()); + } +} diff --git a/src/sqlancer/spark/SparkOptions.java b/src/sqlancer/spark/SparkOptions.java new file mode 100644 index 000000000..7b347ceef --- /dev/null +++ b/src/sqlancer/spark/SparkOptions.java @@ -0,0 +1,43 @@ +package sqlancer.spark; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; + +import sqlancer.DBMSSpecificOptions; +import sqlancer.OracleFactory; +import sqlancer.common.oracle.TLPWhereOracle; +import sqlancer.common.oracle.TestOracle; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.spark.gen.SparkExpressionGenerator; + +@Parameters(separators = "=", commandDescription = "Spark SQL (default port: " + SparkOptions.DEFAULT_PORT + + ", default host: " + SparkOptions.DEFAULT_HOST + ")") +public class SparkOptions implements DBMSSpecificOptions { + public static final String DEFAULT_HOST = "localhost"; + public static final int DEFAULT_PORT = 10000; + + @Parameter(names = "--oracle") + public List oracle = Arrays.asList(SparkOracleFactory.TLPWhere); + + public enum SparkOracleFactory implements OracleFactory { + TLPWhere { + @Override + public TestOracle create(SparkGlobalState globalState) throws SQLException { + SparkExpressionGenerator gen = new SparkExpressionGenerator(globalState); + ExpectedErrors expectedErrors = ExpectedErrors.newErrors().with(SparkErrors.getExpressionErrors()) + .build(); + + return new TLPWhereOracle<>(globalState, gen, expectedErrors); + } + }; + } + + @Override + public List getTestOracleFactory() { + return oracle; + } +} diff --git a/src/sqlancer/spark/SparkProvider.java b/src/sqlancer/spark/SparkProvider.java new file mode 100644 index 000000000..817a92471 --- /dev/null +++ b/src/sqlancer/spark/SparkProvider.java @@ -0,0 +1,123 @@ +package sqlancer.spark; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import com.google.auto.service.AutoService; + +import sqlancer.AbstractAction; +import sqlancer.DatabaseProvider; +import sqlancer.IgnoreMeException; +import sqlancer.MainOptions; +import sqlancer.Randomly; +import sqlancer.SQLConnection; +import sqlancer.SQLProviderAdapter; +import sqlancer.StatementExecutor; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.common.query.SQLQueryProvider; +import sqlancer.spark.gen.SparkInsertGenerator; +import sqlancer.spark.gen.SparkTableGenerator; + +@AutoService(DatabaseProvider.class) +public class SparkProvider extends SQLProviderAdapter { + + public SparkProvider() { + super(SparkGlobalState.class, SparkOptions.class); + } + + public enum Action implements AbstractAction { + INSERT(SparkInsertGenerator::getQuery); + + private final SQLQueryProvider sqlQueryProvider; + + Action(SQLQueryProvider sqlQueryProvider) { + this.sqlQueryProvider = sqlQueryProvider; + } + + @Override + public SQLQueryAdapter getQuery(SparkGlobalState state) throws Exception { + return sqlQueryProvider.getQuery(state); + } + } + + private static int mapActions(SparkGlobalState globalState, Action a) { + Randomly r = globalState.getRandomly(); + switch (a) { + case INSERT: + return r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); + default: + throw new AssertionError(a); + } + } + + @Override + public void generateDatabase(SparkGlobalState globalState) throws Exception { + for (int i = 0; i < Randomly.fromOptions(1, 2); i++) { + boolean success; + do { + String tableName = globalState.getSchema().getFreeTableName(); + SQLQueryAdapter qt = SparkTableGenerator.generate(globalState, tableName); + success = globalState.executeStatement(qt); + } while (!success); + } + + if (globalState.getSchema().getDatabaseTables().isEmpty()) { + throw new IgnoreMeException(); + } + + StatementExecutor se = new StatementExecutor<>(globalState, Action.values(), + SparkProvider::mapActions, (q) -> { + if (globalState.getSchema().getDatabaseTables().isEmpty()) { + throw new IgnoreMeException(); + } + }); + se.executeStatements(); + } + + @Override + public SQLConnection createDatabase(SparkGlobalState globalState) throws SQLException { + String username = globalState.getOptions().getUserName(); + String password = globalState.getOptions().getPassword(); + String host = globalState.getOptions().getHost(); + int port = globalState.getOptions().getPort(); + + if (host == null) { + host = SparkOptions.DEFAULT_HOST; + } + if (port == MainOptions.NO_SET_PORT) { + port = SparkOptions.DEFAULT_PORT; + } + + String databaseName = globalState.getDatabaseName(); + + // Spark uses the Hive driver for JDBC usually + String url = String.format("jdbc:hive2://%s:%d/%s", host, port, "default"); + + // Connect to default to create the fuzzing DB + Connection con = DriverManager.getConnection(url, username, password); + try (Statement s = con.createStatement()) { + s.execute("DROP DATABASE IF EXISTS " + databaseName + " CASCADE"); + } + try (Statement s = con.createStatement()) { + s.execute("CREATE DATABASE " + databaseName); + } + con.close(); + + // Connect to the specific fuzzing DB + con = DriverManager.getConnection(String.format("jdbc:hive2://%s:%d/%s", host, port, databaseName), username, + password); + try (Statement s = con.createStatement()) { + // This allows casting things like BOOLEAN to DATE/TIMESTAMP, which the + // generator loves to do. + s.execute("SET spark.sql.ansi.enabled=false"); + } + return new SQLConnection(con); + } + + @Override + public String getDBMSName() { + return "spark"; + } +} diff --git a/src/sqlancer/spark/SparkSchema.java b/src/sqlancer/spark/SparkSchema.java new file mode 100644 index 000000000..9b3666916 --- /dev/null +++ b/src/sqlancer/spark/SparkSchema.java @@ -0,0 +1,122 @@ +package sqlancer.spark; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.SQLConnection; +import sqlancer.common.schema.AbstractRelationalTable; +import sqlancer.common.schema.AbstractSchema; +import sqlancer.common.schema.AbstractTableColumn; +import sqlancer.common.schema.AbstractTables; +import sqlancer.common.schema.TableIndex; +import sqlancer.spark.SparkSchema.SparkTable; + +public class SparkSchema extends AbstractSchema { + + public enum SparkDataType { + STRING, INTEGER, DOUBLE, BOOLEAN, TIMESTAMP, DATE; + + public static SparkDataType getRandomType() { + return Randomly.fromList(Arrays.asList(values())); + } + } + + public static class SparkColumn extends AbstractTableColumn { + public SparkColumn(String name, SparkTable table, SparkDataType type) { + super(name, table, type); + } + } + + public static class SparkTables extends AbstractTables { + public SparkTables(List tables) { + super(tables); + } + } + + public static class SparkTable extends AbstractRelationalTable { + public SparkTable(String name, List columns, boolean isView) { + super(name, columns, Collections.emptyList(), isView); + } + } + + public SparkSchema(List databaseTables) { + super(databaseTables); + } + + public static SparkSchema fromConnection(SQLConnection con, String databaseName) throws SQLException { + List databaseTables = new ArrayList<>(); + List tableNames = getTableNames(con); + for (String tableName : tableNames) { + List databaseColumns = getTableColumns(con, tableName); + boolean isView = matchesViewName(tableName); + SparkTable t = new SparkTable(tableName, databaseColumns, isView); + for (SparkColumn c : databaseColumns) { + c.setTable(t); + } + databaseTables.add(t); + } + return new SparkSchema(databaseTables); + } + + private static List getTableNames(SQLConnection con) throws SQLException { + List tableNames = new ArrayList<>(); + try (Statement s = con.createStatement()) { + ResultSet tableRs = s.executeQuery("SHOW TABLES"); + while (tableRs.next()) { + // Spark SHOW TABLES output: database, tableName, isTemporary + String tableName = tableRs.getString("tableName"); + tableNames.add(tableName); + } + } + return tableNames; + } + + private static List getTableColumns(SQLConnection con, String tableName) throws SQLException { + List columns = new ArrayList<>(); + try (Statement s = con.createStatement()) { + try (ResultSet rs = s.executeQuery(String.format("DESCRIBE %s", tableName))) { + while (rs.next()) { + String columnName = rs.getString("col_name"); + String dataType = rs.getString("data_type"); + // Filter out Spark partition info or comments usually at bottom of describe + if (columnName.startsWith("#") || columnName.isEmpty()) { + continue; + } + + columns.add(new SparkColumn(columnName, null, getColumnType(dataType))); + } + } + } + return columns; + } + + private static SparkDataType getColumnType(String typeString) { + String upper = typeString.toUpperCase(); + if (upper.startsWith("STRING") || upper.startsWith("VARCHAR") || upper.startsWith("CHAR")) { + return SparkDataType.STRING; + } + if (upper.startsWith("INT") || upper.startsWith("BIGINT") || upper.startsWith("SMALLINT")) { + return SparkDataType.INTEGER; + } + if (upper.startsWith("DOUBLE") || upper.startsWith("FLOAT") || upper.startsWith("DECIMAL")) { + return SparkDataType.DOUBLE; + } + if (upper.startsWith("BOOLEAN")) { + return SparkDataType.BOOLEAN; + } + if (upper.startsWith("TIMESTAMP")) { + return SparkDataType.TIMESTAMP; + } + if (upper.startsWith("DATE")) { + return SparkDataType.DATE; + } + return SparkDataType.STRING; // Fallback + } + +} diff --git a/src/sqlancer/spark/SparkToStringVisitor.java b/src/sqlancer/spark/SparkToStringVisitor.java new file mode 100644 index 000000000..0777c86a6 --- /dev/null +++ b/src/sqlancer/spark/SparkToStringVisitor.java @@ -0,0 +1,121 @@ +package sqlancer.spark; + +import sqlancer.common.ast.newast.NewToStringVisitor; +import sqlancer.common.ast.newast.TableReferenceNode; +import sqlancer.spark.ast.SparkCastOperation; +import sqlancer.spark.ast.SparkConstant; +import sqlancer.spark.ast.SparkExpression; +import sqlancer.spark.ast.SparkJoin; +import sqlancer.spark.ast.SparkSelect; + +public class SparkToStringVisitor extends NewToStringVisitor { + + @Override + public void visitSpecific(SparkExpression expr) { + if (expr instanceof SparkConstant) { + visit((SparkConstant) expr); + } else if (expr instanceof SparkSelect) { + visit((SparkSelect) expr); + } else if (expr instanceof SparkJoin) { + visit((SparkJoin) expr); + } else if (expr instanceof SparkCastOperation) { + visit((SparkCastOperation) expr); + } else { + throw new AssertionError(expr.getClass()); + } + } + + private void visit(SparkConstant constant) { + sb.append(constant.toString()); + } + + private void visit(SparkSelect select) { + sb.append("SELECT "); + if (select.isDistinct()) { + sb.append("DISTINCT "); + } + visit(select.getFetchColumns()); + sb.append(" FROM "); + visit(select.getFromList()); + if (!select.getFromList().isEmpty() && !select.getJoinList().isEmpty()) { + sb.append(", "); + } + if (!select.getJoinList().isEmpty()) { + visit(select.getJoinList()); + } + if (select.getWhereClause() != null) { + sb.append(" WHERE "); + visit(select.getWhereClause()); + } + if (!select.getGroupByExpressions().isEmpty()) { + sb.append(" GROUP BY "); + visit(select.getGroupByExpressions()); + } + if (select.getHavingClause() != null) { + sb.append(" HAVING "); + visit(select.getHavingClause()); + } + if (!select.getOrderByClauses().isEmpty()) { + sb.append(" ORDER BY "); + visit(select.getOrderByClauses()); + } + if (select.getLimitClause() != null) { + sb.append(" LIMIT "); + visit(select.getLimitClause()); + } + // Spark supports OFFSET, though strictly usually with LIMIT or in newer + // versions + if (select.getOffsetClause() != null) { + sb.append(" OFFSET "); + visit(select.getOffsetClause()); + } + } + + private void visit(SparkJoin join) { + switch (join.getJoinType()) { + case INNER: + sb.append(" INNER JOIN "); + break; + case LEFT_OUTER: + sb.append(" LEFT JOIN "); + break; + case RIGHT_OUTER: + sb.append(" RIGHT JOIN "); + break; + case FULL_OUTER: + sb.append(" FULL JOIN "); + break; + case LEFT_SEMI: + sb.append(" LEFT SEMI JOIN "); + break; + // Spark also supports LEFT ANTI, which Hive might lack in some older versions + case LEFT_ANTI: + sb.append(" LEFT ANTI JOIN "); + break; + case CROSS: + sb.append(" CROSS JOIN "); + break; + default: + throw new UnsupportedOperationException("Join type not supported in Spark visitor: " + join.getJoinType()); + } + visit((TableReferenceNode) join.getRightTable()); + if (join.getOnClause() != null) { + sb.append(" ON "); + visit(join.getOnClause()); + } + } + + private void visit(SparkCastOperation cast) { + sb.append("CAST("); + visit(cast.getExpression()); + sb.append(" AS "); + sb.append(cast.getType()); + sb.append(")"); + } + + public static String asString(SparkExpression expr) { + SparkToStringVisitor visitor = new SparkToStringVisitor(); + visitor.visit(expr); + return visitor.get(); + } +} diff --git a/src/sqlancer/spark/ast/SparkBetweenOperation.java b/src/sqlancer/spark/ast/SparkBetweenOperation.java new file mode 100644 index 000000000..59297ba8f --- /dev/null +++ b/src/sqlancer/spark/ast/SparkBetweenOperation.java @@ -0,0 +1,10 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.newast.NewBetweenOperatorNode; + +public class SparkBetweenOperation extends NewBetweenOperatorNode implements SparkExpression { + + public SparkBetweenOperation(SparkExpression left, SparkExpression middle, SparkExpression right, boolean isTrue) { + super(left, middle, right, isTrue); + } +} diff --git a/src/sqlancer/spark/ast/SparkBinaryOperation.java b/src/sqlancer/spark/ast/SparkBinaryOperation.java new file mode 100644 index 000000000..ef3347018 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkBinaryOperation.java @@ -0,0 +1,11 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.BinaryOperatorNode.Operator; +import sqlancer.common.ast.newast.NewBinaryOperatorNode; + +public class SparkBinaryOperation extends NewBinaryOperatorNode implements SparkExpression { + + public SparkBinaryOperation(SparkExpression left, SparkExpression right, Operator op) { + super(left, right, op); + } +} diff --git a/src/sqlancer/spark/ast/SparkCaseOperation.java b/src/sqlancer/spark/ast/SparkCaseOperation.java new file mode 100644 index 000000000..995fd7f52 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkCaseOperation.java @@ -0,0 +1,13 @@ +package sqlancer.spark.ast; + +import java.util.List; + +import sqlancer.common.ast.newast.NewCaseOperatorNode; + +public class SparkCaseOperation extends NewCaseOperatorNode implements SparkExpression { + + public SparkCaseOperation(SparkExpression switchCondition, List conditions, + List expressions, SparkExpression elseExpr) { + super(switchCondition, conditions, expressions, elseExpr); + } +} diff --git a/src/sqlancer/spark/ast/SparkCastOperation.java b/src/sqlancer/spark/ast/SparkCastOperation.java new file mode 100644 index 000000000..547551285 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkCastOperation.java @@ -0,0 +1,25 @@ +package sqlancer.spark.ast; + +import sqlancer.spark.SparkSchema.SparkDataType; + +public class SparkCastOperation implements SparkExpression { + + private final SparkExpression expression; + private final SparkDataType type; + + public SparkCastOperation(SparkExpression expression, SparkDataType type) { + if (expression == null) { + throw new AssertionError(); + } + this.expression = expression; + this.type = type; + } + + public SparkExpression getExpression() { + return expression; + } + + public SparkDataType getType() { + return type; + } +} diff --git a/src/sqlancer/spark/ast/SparkColumnReference.java b/src/sqlancer/spark/ast/SparkColumnReference.java new file mode 100644 index 000000000..ccd1b7855 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkColumnReference.java @@ -0,0 +1,11 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.newast.ColumnReferenceNode; +import sqlancer.spark.SparkSchema.SparkColumn; + +public class SparkColumnReference extends ColumnReferenceNode implements SparkExpression { + + public SparkColumnReference(SparkColumn column) { + super(column); + } +} diff --git a/src/sqlancer/spark/ast/SparkConstant.java b/src/sqlancer/spark/ast/SparkConstant.java new file mode 100644 index 000000000..84a397624 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkConstant.java @@ -0,0 +1,194 @@ +package sqlancer.spark.ast; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; + +public abstract class SparkConstant implements SparkExpression { + + public boolean isNull() { + return false; + } + + public static class SparkNullConstant extends SparkConstant { + + @Override + public boolean isNull() { + return true; + } + + @Override + public String toString() { + return "NULL"; + } + } + + public static class SparkIntConstant extends SparkConstant { + + private final long value; + + public SparkIntConstant(long value) { + this.value = value; + } + + public long getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + + public static class SparkDoubleConstant extends SparkConstant { + + private final double value; + + public SparkDoubleConstant(double value) { + this.value = value; + } + + public double getValue() { + return value; + } + + @Override + public String toString() { + if (value == Double.POSITIVE_INFINITY) { + return "CAST('Infinity' AS DOUBLE)"; + } else if (value == Double.NEGATIVE_INFINITY) { + return "CAST('-Infinity' AS DOUBLE)"; + } else if (Double.isNaN(value)) { + return "CAST('NaN' AS DOUBLE)"; + } + return String.valueOf(value); + } + } + + public static class SparkDecimalConstant extends SparkConstant { + + private final BigDecimal value; + + public SparkDecimalConstant(BigDecimal value) { + this.value = value; + } + + public BigDecimal getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + + public static class SparkTimestampConstant extends SparkConstant { + + private final String textRepr; + + public SparkTimestampConstant(long value) { + Timestamp timestamp = new Timestamp(value); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // Spark prefers full timestamp + this.textRepr = dateFormat.format(timestamp); + } + + public String getValue() { + return textRepr; + } + + @Override + public String toString() { + return String.format("TIMESTAMP '%s'", textRepr); + } + } + + public static class SparkDateConstant extends SparkConstant { + + private final String textRepr; + + public SparkDateConstant(long value) { + Timestamp timestamp = new Timestamp(value); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + this.textRepr = dateFormat.format(timestamp); + } + + public String getValue() { + return textRepr; + } + + @Override + public String toString() { + return String.format("DATE '%s'", textRepr); + } + } + + public static class SparkStringConstant extends SparkConstant { + + private final String value; + + public SparkStringConstant(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return "'" + value.replace("'", "''").replace("\\", "\\\\") + "'"; + } + } + + public static class SparkBooleanConstant extends SparkConstant { + + private final boolean value; + + public SparkBooleanConstant(boolean value) { + this.value = value; + } + + public boolean getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + + public static SparkConstant createNullConstant() { + return new SparkNullConstant(); + } + + public static SparkConstant createIntConstant(long value) { + return new SparkIntConstant(value); + } + + public static SparkConstant createDoubleConstant(double value) { + return new SparkDoubleConstant(value); + } + + public static SparkConstant createDecimalConstant(BigDecimal value) { + return new SparkDecimalConstant(value); + } + + public static SparkConstant createTimestampConstant(long value) { + return new SparkTimestampConstant(value); + } + + public static SparkConstant createDateConstant(long value) { + return new SparkDateConstant(value); + } + + public static SparkConstant createStringConstant(String value) { + return new SparkStringConstant(value); + } + + public static SparkConstant createBooleanConstant(boolean value) { + return new SparkBooleanConstant(value); + } +} diff --git a/src/sqlancer/spark/ast/SparkExpression.java b/src/sqlancer/spark/ast/SparkExpression.java new file mode 100644 index 000000000..3872ccfda --- /dev/null +++ b/src/sqlancer/spark/ast/SparkExpression.java @@ -0,0 +1,7 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.newast.Expression; +import sqlancer.spark.SparkSchema.SparkColumn; + +public interface SparkExpression extends Expression { +} diff --git a/src/sqlancer/spark/ast/SparkFunction.java b/src/sqlancer/spark/ast/SparkFunction.java new file mode 100644 index 000000000..d5740ee36 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkFunction.java @@ -0,0 +1,13 @@ +package sqlancer.spark.ast; + +import java.util.List; + +import sqlancer.common.ast.newast.NewFunctionNode; + +public class SparkFunction extends NewFunctionNode implements SparkExpression { + + public SparkFunction(List args, F func) { + super(args, func); + } + +} diff --git a/src/sqlancer/spark/ast/SparkInOperation.java b/src/sqlancer/spark/ast/SparkInOperation.java new file mode 100644 index 000000000..430d9b5c2 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkInOperation.java @@ -0,0 +1,12 @@ +package sqlancer.spark.ast; + +import java.util.List; + +import sqlancer.common.ast.newast.NewInOperatorNode; + +public class SparkInOperation extends NewInOperatorNode implements SparkExpression { + + public SparkInOperation(SparkExpression left, List right, boolean isNegated) { + super(left, right, isNegated); + } +} diff --git a/src/sqlancer/spark/ast/SparkJoin.java b/src/sqlancer/spark/ast/SparkJoin.java new file mode 100644 index 000000000..a59eaff48 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkJoin.java @@ -0,0 +1,46 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.newast.Join; +import sqlancer.spark.SparkSchema.SparkColumn; +import sqlancer.spark.SparkSchema.SparkTable; + +public class SparkJoin implements SparkExpression, Join { + + private final SparkTableReference leftTable; + private final SparkTableReference rightTable; + private final JoinType joinType; + private SparkExpression onClause; + + public enum JoinType { + INNER, LEFT_OUTER, RIGHT_OUTER, FULL_OUTER, LEFT_SEMI, LEFT_ANTI, CROSS; + } + + public SparkJoin(SparkTableReference leftTable, SparkTableReference rightTable, JoinType joinType, + SparkExpression onClause) { + this.leftTable = leftTable; + this.rightTable = rightTable; + this.joinType = joinType; + this.onClause = onClause; + } + + public SparkTableReference getLeftTable() { + return leftTable; + } + + public SparkTableReference getRightTable() { + return rightTable; + } + + public JoinType getJoinType() { + return joinType; + } + + public SparkExpression getOnClause() { + return onClause; + } + + @Override + public void setOnClause(SparkExpression onClause) { + this.onClause = onClause; + } +} diff --git a/src/sqlancer/spark/ast/SparkOrderingTerm.java b/src/sqlancer/spark/ast/SparkOrderingTerm.java new file mode 100644 index 000000000..870c8239b --- /dev/null +++ b/src/sqlancer/spark/ast/SparkOrderingTerm.java @@ -0,0 +1,10 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.newast.NewOrderingTerm; + +public class SparkOrderingTerm extends NewOrderingTerm implements SparkExpression { + + public SparkOrderingTerm(SparkExpression expr, Ordering ordering) { + super(expr, ordering); + } +} diff --git a/src/sqlancer/spark/ast/SparkSelect.java b/src/sqlancer/spark/ast/SparkSelect.java new file mode 100644 index 000000000..8b59f5513 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkSelect.java @@ -0,0 +1,42 @@ +package sqlancer.spark.ast; + +import java.util.List; +import java.util.stream.Collectors; + +import sqlancer.common.ast.SelectBase; +import sqlancer.common.ast.newast.Select; +import sqlancer.spark.SparkSchema.SparkColumn; +import sqlancer.spark.SparkSchema.SparkTable; +import sqlancer.spark.SparkToStringVisitor; + +public class SparkSelect extends SelectBase + implements Select, SparkExpression { + + private boolean isDistinct; + + public void setDistinct(boolean isDistinct) { + this.isDistinct = isDistinct; + } + + public boolean isDistinct() { + return isDistinct; + } + + @Override + public void setJoinClauses(List joinStatements) { + List expressions = joinStatements.stream().map(e -> (SparkExpression) e) + .collect(Collectors.toList()); + setJoinList(expressions); + } + + @Override + public List getJoinClauses() { + return getJoinList().stream().map(e -> (SparkJoin) e).collect(Collectors.toList()); + } + + @Override + public String asString() { + return SparkToStringVisitor.asString(this); + } + +} diff --git a/src/sqlancer/spark/ast/SparkTableReference.java b/src/sqlancer/spark/ast/SparkTableReference.java new file mode 100644 index 000000000..5bcbb5d03 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkTableReference.java @@ -0,0 +1,13 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.newast.TableReferenceNode; +import sqlancer.spark.SparkSchema; + +public class SparkTableReference extends TableReferenceNode + implements SparkExpression { + + public SparkTableReference(SparkSchema.SparkTable table) { + super(table); + } + +} diff --git a/src/sqlancer/spark/ast/SparkUnaryPostfixOperation.java b/src/sqlancer/spark/ast/SparkUnaryPostfixOperation.java new file mode 100644 index 000000000..3dd9d28e2 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkUnaryPostfixOperation.java @@ -0,0 +1,13 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.BinaryOperatorNode.Operator; +import sqlancer.common.ast.newast.NewUnaryPostfixOperatorNode; + +public class SparkUnaryPostfixOperation extends NewUnaryPostfixOperatorNode + implements SparkExpression { + + public SparkUnaryPostfixOperation(SparkExpression expr, Operator op) { + super(expr, op); + } + +} diff --git a/src/sqlancer/spark/ast/SparkUnaryPrefixOperation.java b/src/sqlancer/spark/ast/SparkUnaryPrefixOperation.java new file mode 100644 index 000000000..5c1a8e4c6 --- /dev/null +++ b/src/sqlancer/spark/ast/SparkUnaryPrefixOperation.java @@ -0,0 +1,12 @@ +package sqlancer.spark.ast; + +import sqlancer.common.ast.BinaryOperatorNode.Operator; +import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; + +public class SparkUnaryPrefixOperation extends NewUnaryPrefixOperatorNode implements SparkExpression { + + public SparkUnaryPrefixOperation(SparkExpression expr, Operator op) { + super(expr, op); + } + +} diff --git a/src/sqlancer/spark/gen/SparkExpressionGenerator.java b/src/sqlancer/spark/gen/SparkExpressionGenerator.java new file mode 100644 index 000000000..3708f314a --- /dev/null +++ b/src/sqlancer/spark/gen/SparkExpressionGenerator.java @@ -0,0 +1,336 @@ +package sqlancer.spark.gen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import sqlancer.Randomly; +import sqlancer.common.ast.BinaryOperatorNode.Operator; +import sqlancer.common.ast.newast.NewOrderingTerm.Ordering; +import sqlancer.common.gen.TLPWhereGenerator; +import sqlancer.common.gen.UntypedExpressionGenerator; +import sqlancer.common.schema.AbstractTables; +import sqlancer.spark.SparkGlobalState; +import sqlancer.spark.SparkSchema.SparkColumn; +import sqlancer.spark.SparkSchema.SparkDataType; +import sqlancer.spark.SparkSchema.SparkTable; +import sqlancer.spark.ast.SparkBetweenOperation; +import sqlancer.spark.ast.SparkBinaryOperation; +import sqlancer.spark.ast.SparkCaseOperation; +import sqlancer.spark.ast.SparkCastOperation; +import sqlancer.spark.ast.SparkColumnReference; +import sqlancer.spark.ast.SparkConstant; +import sqlancer.spark.ast.SparkExpression; +import sqlancer.spark.ast.SparkFunction; +import sqlancer.spark.ast.SparkInOperation; +import sqlancer.spark.ast.SparkJoin; +import sqlancer.spark.ast.SparkOrderingTerm; +import sqlancer.spark.ast.SparkSelect; +import sqlancer.spark.ast.SparkTableReference; +import sqlancer.spark.ast.SparkUnaryPostfixOperation; +import sqlancer.spark.ast.SparkUnaryPrefixOperation; + +public class SparkExpressionGenerator extends UntypedExpressionGenerator + implements TLPWhereGenerator { + + private final SparkGlobalState globalState; + private List tables; + + private enum Expression { + UNARY_PREFIX, UNARY_POSTFIX, BINARY_COMPARISON, BINARY_LOGICAL, BINARY_ARITHMETIC, CAST, FUNC, BETWEEN, IN, + CASE; + } + + public SparkExpressionGenerator(SparkGlobalState globalState) { + this.globalState = globalState; + } + + @Override + public SparkExpression negatePredicate(SparkExpression predicate) { + return new SparkUnaryPrefixOperation(predicate, SparkUnaryPrefixOperator.NOT); + } + + @Override + public SparkExpression isNull(SparkExpression expr) { + return new SparkUnaryPostfixOperation(expr, SparkUnaryPostfixOperator.IS_NULL); + } + + @Override + protected SparkExpression generateExpression(int depth) { + return generateExpressionInternal(depth); + } + + private SparkExpression generateExpressionInternal(int depth) throws AssertionError { + if (depth >= globalState.getOptions().getMaxExpressionDepth() + || Randomly.getBooleanWithRatherLowProbability()) { + return generateLeafNode(); + } + if (allowAggregates && Randomly.getBooleanWithRatherLowProbability()) { + allowAggregates = false; // aggregate function calls cannot be nested + SparkAggregateFunction aggregate = SparkAggregateFunction.getRandom(); + return new SparkFunction<>(generateExpressions(aggregate.getNrArgs(), depth + 1), aggregate); + } + + List possibleOptions = new ArrayList<>(Arrays.asList(Expression.values())); + Expression expr = Randomly.fromList(possibleOptions); + + switch (expr) { + case UNARY_PREFIX: + return new SparkUnaryPrefixOperation(generateExpression(depth + 1), SparkUnaryPrefixOperator.getRandom()); + case UNARY_POSTFIX: + return new SparkUnaryPostfixOperation(generateExpression(depth + 1), SparkUnaryPostfixOperator.getRandom()); + case BINARY_COMPARISON: + Operator op = SparkBinaryComparisonOperator.getRandom(); + return new SparkBinaryOperation(generateExpression(depth + 1), generateExpression(depth + 1), op); + case BINARY_LOGICAL: + op = SparkBinaryLogicalOperator.getRandom(); + return new SparkBinaryOperation(generateExpression(depth + 1), generateExpression(depth + 1), op); + case BINARY_ARITHMETIC: + return new SparkBinaryOperation(generateExpression(depth + 1), generateExpression(depth + 1), + SparkBinaryArithmeticOperator.getRandom()); + case CAST: + return new SparkCastOperation(generateExpression(depth + 1), SparkDataType.getRandomType()); + case FUNC: + SparkFunc func = SparkFunc.getRandom(); + return new SparkFunction<>(generateExpressions(func.getNrArgs()), func); + case BETWEEN: + return new SparkBetweenOperation(generateExpression(depth + 1), generateExpression(depth + 1), + generateExpression(depth + 1), Randomly.getBoolean()); + case IN: + return new SparkInOperation(generateExpression(depth + 1), + generateExpressions(Randomly.smallNumber() + 1, depth + 1), Randomly.getBoolean()); + case CASE: + int nr = Randomly.smallNumber() + 1; + return new SparkCaseOperation(generateExpression(depth + 1), generateExpressions(nr, depth + 1), + generateExpressions(nr, depth + 1), generateExpression(depth + 1)); + default: + throw new AssertionError(expr); + } + } + + @Override + public SparkExpression generateConstant() { + if (Randomly.getBooleanWithRatherLowProbability()) { + return SparkConstant.createNullConstant(); + } + SparkDataType[] values = SparkDataType.values(); + SparkDataType constantType = Randomly.fromOptions(values); + switch (constantType) { + case STRING: + return SparkConstant.createStringConstant(globalState.getRandomly().getString()); + case INTEGER: + return SparkConstant.createIntConstant(globalState.getRandomly().getInteger()); + case DOUBLE: + return SparkConstant.createDoubleConstant(globalState.getRandomly().getDouble()); + case BOOLEAN: + return SparkConstant.createBooleanConstant(Randomly.getBoolean()); + case TIMESTAMP: + return SparkConstant.createTimestampConstant(globalState.getRandomly().getInteger()); + case DATE: + return SparkConstant.createDateConstant(globalState.getRandomly().getInteger()); + default: + throw new AssertionError(constantType); + } + } + + @Override + protected SparkExpression generateColumn() { + SparkColumn column = Randomly.fromList(columns); + return new SparkColumnReference(column); + } + + @Override + public List generateOrderBys() { + List expr = super.generateOrderBys(); + List newExpr = new ArrayList<>(expr.size()); + for (SparkExpression curExpr : expr) { + if (Randomly.getBoolean()) { + curExpr = new SparkOrderingTerm(curExpr, Ordering.getRandom()); + } + newExpr.add(curExpr); + } + return newExpr; + } + + @Override + public SparkExpressionGenerator setTablesAndColumns(AbstractTables tables) { + this.columns = tables.getColumns(); + this.tables = tables.getTables(); + return this; + } + + @Override + public SparkExpression generateBooleanExpression() { + return generateExpression(); + } + + @Override + public SparkSelect generateSelect() { + return new SparkSelect(); + } + + @Override + public List getTableRefs() { + return tables.stream().map(t -> new SparkTableReference(t)).collect(Collectors.toList()); + } + + @Override + public List generateFetchColumns(boolean allowAggregates) { + if (Randomly.getBoolean()) { + return List.of(new SparkColumnReference(new SparkColumn("*", null, null))); + } + return Randomly.nonEmptySubset(columns).stream().map(c -> new SparkColumnReference(c)) + .collect(Collectors.toList()); + } + + @Override + public List getRandomJoinClauses() { + return List.of(); + } + + public enum SparkUnaryPrefixOperator implements Operator { + NOT("NOT"), PLUS("+"), MINUS("-"), BITWISE_NOT("~"); + + private String textRepr; + + SparkUnaryPrefixOperator(String textRepr) { + this.textRepr = textRepr; + } + + public static SparkUnaryPrefixOperator getRandom() { + return Randomly.fromOptions(values()); + } + + @Override + public String getTextRepresentation() { + return textRepr; + } + } + + public enum SparkUnaryPostfixOperator implements Operator { + IS_NULL("IS NULL"), IS_NOT_NULL("IS NOT NULL"); + + private String textRepr; + + SparkUnaryPostfixOperator(String textRepr) { + this.textRepr = textRepr; + } + + public static SparkUnaryPostfixOperator getRandom() { + return Randomly.fromOptions(values()); + } + + @Override + public String getTextRepresentation() { + return textRepr; + } + } + + public enum SparkBinaryComparisonOperator implements Operator { + EQUALS("="), GREATER(">"), GREATER_EQUALS(">="), SMALLER("<"), SMALLER_EQUALS("<="), NOT_EQUALS("!="), + LIKE("LIKE"), NOT_LIKE("NOT LIKE"), RLIKE("RLIKE"); + + private String textRepr; + + SparkBinaryComparisonOperator(String textRepr) { + this.textRepr = textRepr; + } + + public static SparkBinaryComparisonOperator getRandom() { + return Randomly.fromOptions(values()); + } + + @Override + public String getTextRepresentation() { + return textRepr; + } + } + + public enum SparkBinaryLogicalOperator implements Operator { + AND("AND"), OR("OR"); + + private String textRepr; + + SparkBinaryLogicalOperator(String textRepr) { + this.textRepr = textRepr; + } + + public static SparkBinaryLogicalOperator getRandom() { + return Randomly.fromOptions(values()); + } + + @Override + public String getTextRepresentation() { + return textRepr; + } + } + + public enum SparkBinaryArithmeticOperator implements Operator { + // Spark supports || for concat, and bitwise operators &, |, ^ + CONCAT("||"), ADD("+"), SUB("-"), MULT("*"), DIV("/"), MOD("%"), BITWISE_AND("&"), BITWISE_OR("|"), + BITWISE_XOR("^"); + + private String textRepr; + + SparkBinaryArithmeticOperator(String textRepr) { + this.textRepr = textRepr; + } + + public static SparkBinaryArithmeticOperator getRandom() { + return Randomly.fromOptions(values()); + } + + @Override + public String getTextRepresentation() { + return textRepr; + } + } + + public enum SparkAggregateFunction { + COUNT(1), SUM(1), AVG(1), MIN(1), MAX(1), VARIANCE(1), VAR_SAMP(1), STDDEV_POP(1), STDDEV_SAMP(1), COVAR_POP(2), + COVAR_SAMP(2), CORR(2); + + private int nrArgs; + + SparkAggregateFunction(int nrArgs) { + this.nrArgs = nrArgs; + } + + public static SparkAggregateFunction getRandom() { + return Randomly.fromOptions(values()); + } + + public int getNrArgs() { + return nrArgs; + } + } + + public enum SparkFunc { + ROUND(2), FLOOR(1), ABS(1), CEIL(1); + + private int nrArgs; + private boolean isVariadic; + + SparkFunc(int nrArgs) { + this(nrArgs, false); + } + + SparkFunc(int nrArgs, boolean isVariadic) { + this.nrArgs = nrArgs; + this.isVariadic = isVariadic; + } + + public static SparkFunc getRandom() { + return Randomly.fromOptions(values()); + } + + public int getNrArgs() { + if (isVariadic) { + return Randomly.smallNumber() + nrArgs; + } else { + return nrArgs; + } + } + } +} diff --git a/src/sqlancer/spark/gen/SparkInsertGenerator.java b/src/sqlancer/spark/gen/SparkInsertGenerator.java new file mode 100644 index 000000000..c43584315 --- /dev/null +++ b/src/sqlancer/spark/gen/SparkInsertGenerator.java @@ -0,0 +1,46 @@ +package sqlancer.spark.gen; + +import java.util.List; + +import sqlancer.common.gen.AbstractInsertGenerator; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.spark.SparkErrors; +import sqlancer.spark.SparkGlobalState; +import sqlancer.spark.SparkSchema.SparkColumn; +import sqlancer.spark.SparkSchema.SparkTable; +import sqlancer.spark.SparkToStringVisitor; + +public class SparkInsertGenerator extends AbstractInsertGenerator { + + private final SparkGlobalState globalState; + private final SparkExpressionGenerator gen; + + public SparkInsertGenerator(SparkGlobalState globalState) { + this.globalState = globalState; + this.gen = new SparkExpressionGenerator(globalState); + this.canonicalizeString = false; + } + + public static SQLQueryAdapter getQuery(SparkGlobalState globalState) { + return new SparkInsertGenerator(globalState).getStatement(); + } + + @Override + protected void insertValue(SparkColumn column) { + sb.append(SparkToStringVisitor.asString(gen.generateConstant())); + } + + @Override + public void buildStatement() { + sb.append("INSERT INTO "); + SparkTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); + sb.append(table.getName()); + + sb.append(" VALUES "); + + List columns = table.getColumns(); + insertColumns(columns); + + SparkErrors.addInsertErrors(errors); + } +} diff --git a/src/sqlancer/spark/gen/SparkTableGenerator.java b/src/sqlancer/spark/gen/SparkTableGenerator.java new file mode 100644 index 000000000..2c26ea34c --- /dev/null +++ b/src/sqlancer/spark/gen/SparkTableGenerator.java @@ -0,0 +1,95 @@ +package sqlancer.spark.gen; + +import java.util.ArrayList; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.common.DBMSCommon; +import sqlancer.common.gen.AbstractTableGenerator; +import sqlancer.common.query.SQLQueryAdapter; +import sqlancer.spark.SparkErrors; +import sqlancer.spark.SparkGlobalState; +import sqlancer.spark.SparkSchema; +import sqlancer.spark.SparkSchema.SparkColumn; +import sqlancer.spark.SparkSchema.SparkDataType; +import sqlancer.spark.SparkSchema.SparkTable; +import sqlancer.spark.SparkToStringVisitor; + +public class SparkTableGenerator extends AbstractTableGenerator { + + private enum ColumnConstraints { + NOT_NULL, DEFAULT + // PRIMARY KEY and UNIQUE are often not supported in standard Spark file sources + // (Parquet/ORC) + // without specific catalogs (like Delta/Iceberg), so we limit to constraints + // Spark SQL widely accepts. + } + + private final SparkGlobalState globalState; + private final String tableName; + private final SparkExpressionGenerator gen; + private final SparkTable table; + private final List columnsToBeAdded = new ArrayList<>(); + + public SparkTableGenerator(SparkGlobalState globalState, String tableName) { + this.tableName = tableName; + this.globalState = globalState; + this.table = new SparkTable(tableName, columnsToBeAdded, false); + this.gen = new SparkExpressionGenerator(globalState).setColumns(columnsToBeAdded); + this.canAffectSchema = true; + this.canonicalizeString = false; + } + + public static SQLQueryAdapter generate(SparkGlobalState globalState, String tableName) { + return new SparkTableGenerator(globalState, tableName).getStatement(); + } + + @Override + public void buildStatement() { + int columnCount = Randomly.smallNumber() + 1; + for (int i = 0; i < columnCount; i++) { + String columnName = DBMSCommon.createColumnName(i); + SparkDataType type = SparkSchema.SparkDataType.getRandomType(); + columnsToBeAdded.add(new SparkColumn(columnName, table, type)); + } + appendCreateTable(globalState.getDatabaseName() + "." + tableName); + sb.append(" "); + appendColumnDefinitions(columnsToBeAdded); + sb.append(" USING PARQUET"); + + // TODO: implement PARTITION BY clause + // TODO: implement CLUSTERED BY clauses + // TODO: implement ROW FORMAT and STORED AS clauses + // TODO: randomly add some predefined TABLEPROPERTIES + + SparkErrors.addExpressionErrors(errors); + } + + @Override + protected void appendColumnDefinition(SparkColumn column) { + sb.append(column.getName()); + sb.append(" "); + sb.append(column.getType()); + appendColumnConstraint(); + } + + private void appendColumnConstraint() { + if (Randomly.getBoolean()) { + return; + } + + ColumnConstraints constraint = Randomly.fromOptions(ColumnConstraints.values()); + switch (constraint) { + case NOT_NULL: + sb.append(" NOT NULL"); + break; + case DEFAULT: + sb.append(" DEFAULT "); + sb.append(SparkToStringVisitor.asString(gen.generateConstant())); + sb.append(" "); + break; + default: + throw new AssertionError(constraint); + } + } +} diff --git a/src/sqlancer/sqlite3/gen/dml/SQLite3DeleteGenerator.java b/src/sqlancer/sqlite3/gen/dml/SQLite3DeleteGenerator.java index 4c86417d0..a19da484f 100644 --- a/src/sqlancer/sqlite3/gen/dml/SQLite3DeleteGenerator.java +++ b/src/sqlancer/sqlite3/gen/dml/SQLite3DeleteGenerator.java @@ -3,7 +3,7 @@ import java.util.Arrays; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.sqlite3.SQLite3Errors; import sqlancer.sqlite3.SQLite3GlobalState; @@ -11,26 +11,33 @@ import sqlancer.sqlite3.gen.SQLite3ExpressionGenerator; import sqlancer.sqlite3.schema.SQLite3Schema.SQLite3Table; -public final class SQLite3DeleteGenerator { +public final class SQLite3DeleteGenerator extends AbstractDeleteGenerator { - private SQLite3DeleteGenerator() { + private final SQLite3GlobalState globalState; + private final SQLite3Table table; + + private SQLite3DeleteGenerator(SQLite3GlobalState globalState, SQLite3Table table) { + this.globalState = globalState; + this.table = table; + this.canAffectSchema = true; } public static SQLQueryAdapter deleteContent(SQLite3GlobalState globalState) { - SQLite3Table tableName = globalState.getSchema().getRandomTable(t -> !t.isView() && !t.isReadOnly()); - return deleteContent(globalState, tableName); + SQLite3Table table = globalState.getSchema().getRandomTable(t -> !t.isView() && !t.isReadOnly()); + return deleteContent(globalState, table); + } + + public static SQLQueryAdapter deleteContent(SQLite3GlobalState globalState, SQLite3Table table) { + return new SQLite3DeleteGenerator(globalState, table).getStatement(); } - public static SQLQueryAdapter deleteContent(SQLite3GlobalState globalState, SQLite3Table tableName) { - StringBuilder sb = new StringBuilder(); - sb.append("DELETE FROM "); - sb.append(tableName.getName()); + @Override + public void buildStatement() { + appendDeleteFromTable(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(SQLite3Visitor.asString(new SQLite3ExpressionGenerator(globalState) - .setColumns(tableName.getColumns()).generateExpression())); + appendWhereClause(SQLite3Visitor.asString( + new SQLite3ExpressionGenerator(globalState).setColumns(table.getColumns()).generateExpression())); } - ExpectedErrors errors = new ExpectedErrors(); SQLite3Errors.addExpectedExpressionErrors(errors); errors.addAll(Arrays.asList("[SQLITE_ERROR] SQL error or missing database (foreign key mismatch", "[SQLITE_CONSTRAINT] Abort due to constraint violation ", @@ -40,7 +47,6 @@ public static SQLQueryAdapter deleteContent(SQLite3GlobalState globalState, SQLi "cannot INSERT into generated column", "A table in the database is locked", "load_extension() prohibited in triggers and views", "The database file is locked")); SQLite3Errors.addDeleteErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors, true); } } diff --git a/src/sqlancer/sqlite3/gen/dml/SQLite3UpdateGenerator.java b/src/sqlancer/sqlite3/gen/dml/SQLite3UpdateGenerator.java index 5a17ad339..9230f47ba 100644 --- a/src/sqlancer/sqlite3/gen/dml/SQLite3UpdateGenerator.java +++ b/src/sqlancer/sqlite3/gen/dml/SQLite3UpdateGenerator.java @@ -14,14 +14,17 @@ import sqlancer.sqlite3.schema.SQLite3Schema.SQLite3Column; import sqlancer.sqlite3.schema.SQLite3Schema.SQLite3Table; -public class SQLite3UpdateGenerator extends AbstractUpdateGenerator { +public final class SQLite3UpdateGenerator extends AbstractUpdateGenerator { private final SQLite3GlobalState globalState; private final Randomly r; + private final SQLite3Table table; - public SQLite3UpdateGenerator(SQLite3GlobalState globalState, Randomly r) { + private SQLite3UpdateGenerator(SQLite3GlobalState globalState, SQLite3Table table) { this.globalState = globalState; - this.r = r; + this.r = globalState.getRandomly(); + this.table = table; + this.canAffectSchema = true; } public static SQLQueryAdapter updateRow(SQLite3GlobalState globalState) { @@ -31,11 +34,11 @@ public static SQLQueryAdapter updateRow(SQLite3GlobalState globalState) { } public static SQLQueryAdapter updateRow(SQLite3GlobalState globalState, SQLite3Table table) { - SQLite3UpdateGenerator generator = new SQLite3UpdateGenerator(globalState, globalState.getRandomly()); - return generator.generate(table); + return new SQLite3UpdateGenerator(globalState, table).getStatement(); } - private SQLQueryAdapter generate(SQLite3Table table) { + @Override + public void buildStatement() { List columnsToUpdate = Randomly.nonEmptySubsetPotentialDuplicates(table.getColumns()); sb.append("UPDATE "); if (Randomly.getBoolean()) { @@ -73,10 +76,9 @@ private SQLQueryAdapter generate(SQLite3Table table) { } if (Randomly.getBoolean()) { - sb.append(" WHERE "); String whereClause = SQLite3Visitor.asString( new SQLite3ExpressionGenerator(globalState).setColumns(table.getColumns()).generateExpression()); - sb.append(whereClause); + appendWhereClause(whereClause); } // ORDER BY and LIMIT are only supported by enabling a compile-time option @@ -98,8 +100,6 @@ private SQLQueryAdapter generate(SQLite3Table table) { SQLite3Errors.addInsertNowErrors(errors); SQLite3Errors.addExpectedExpressionErrors(errors); SQLite3Errors.addDeleteErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors, true /* column could have an ON UPDATE clause */); - } @Override diff --git a/src/sqlancer/sqlite3/schema/SQLite3Schema.java b/src/sqlancer/sqlite3/schema/SQLite3Schema.java index 586d7e776..fc97929d3 100644 --- a/src/sqlancer/sqlite3/schema/SQLite3Schema.java +++ b/src/sqlancer/sqlite3/schema/SQLite3Schema.java @@ -78,6 +78,7 @@ public SQLite3Column(String rowId, SQLite3DataType columnType, boolean isInteger this.generated = generated; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } diff --git a/src/sqlancer/tidb/TiDBSchema.java b/src/sqlancer/tidb/TiDBSchema.java index 32e00504b..4ce7306e5 100644 --- a/src/sqlancer/tidb/TiDBSchema.java +++ b/src/sqlancer/tidb/TiDBSchema.java @@ -170,6 +170,7 @@ public TiDBColumn(String name, TiDBCompositeDataType columnType, boolean isPrima this.hasDefault = hasDefault; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } @@ -297,10 +298,6 @@ public TiDBTable(String tableName, List columns, List in super(tableName, columns, indexes, isView); } - public boolean hasPrimaryKey() { - return getColumns().stream().anyMatch(c -> c.isPrimaryKey()); - } - } public static TiDBSchema fromConnection(SQLConnection con, String databaseName) throws SQLException { @@ -313,7 +310,7 @@ public static TiDBSchema fromConnection(SQLConnection con, String databaseName) continue; } List indexes = getIndexes(con, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); TiDBTable t = new TiDBTable(tableName, databaseColumns, indexes, isView); for (TiDBColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/tidb/gen/TiDBDeleteGenerator.java b/src/sqlancer/tidb/gen/TiDBDeleteGenerator.java index c3986f8d9..c83d1a6eb 100644 --- a/src/sqlancer/tidb/gen/TiDBDeleteGenerator.java +++ b/src/sqlancer/tidb/gen/TiDBDeleteGenerator.java @@ -1,10 +1,9 @@ package sqlancer.tidb.gen; -import java.sql.SQLException; import java.util.stream.Collectors; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.tidb.TiDBErrors; import sqlancer.tidb.TiDBExpressionGenerator; @@ -12,16 +11,24 @@ import sqlancer.tidb.TiDBSchema.TiDBTable; import sqlancer.tidb.visitor.TiDBVisitor; -public final class TiDBDeleteGenerator { +public final class TiDBDeleteGenerator extends AbstractDeleteGenerator { - private TiDBDeleteGenerator() { + private final TiDBGlobalState globalState; + + private TiDBDeleteGenerator(TiDBGlobalState globalState) { + this.globalState = globalState; + } + + public static SQLQueryAdapter getQuery(TiDBGlobalState globalState) { + return new TiDBDeleteGenerator(globalState).getStatement(); } - public static SQLQueryAdapter getQuery(TiDBGlobalState globalState) throws SQLException { - ExpectedErrors errors = ExpectedErrors.newErrors().with(TiDBErrors.getExpressionErrors()).build(); + @Override + public void buildStatement() { + errors.addAll(TiDBErrors.getExpressionErrors()); TiDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); TiDBExpressionGenerator gen = new TiDBExpressionGenerator(globalState).setColumns(table.getColumns()); - StringBuilder sb = new StringBuilder("DELETE "); + sb.append("DELETE "); if (Randomly.getBooleanWithSmallProbability()) { sb.append("LOW_PRIORITY "); } @@ -34,8 +41,7 @@ public static SQLQueryAdapter getQuery(TiDBGlobalState globalState) throws SQLEx sb.append("FROM "); sb.append(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(TiDBVisitor.asString(gen.generateExpression())); + appendWhereClause(TiDBVisitor.asString(gen.generateExpression())); errors.add("Truncated incorrect"); errors.add("Data truncation"); errors.add("Truncated incorrect FLOAT value"); @@ -47,16 +53,13 @@ public static SQLQueryAdapter getQuery(TiDBGlobalState globalState) throws SQLEx .collect(Collectors.joining(", "))); } if (Randomly.getBoolean()) { - sb.append(" LIMIT "); - sb.append(Randomly.getNotCachedInteger(0, Integer.MAX_VALUE)); + appendLimitClause(Randomly.getNotCachedInteger(0, Integer.MAX_VALUE)); } errors.add("Bad Number"); errors.add("Truncated incorrect"); // https://github.com/pingcap/tidb/issues/24292 errors.add("is not valid for CHARACTER SET"); errors.add("Division by 0"); errors.add("error parsing regexp"); - return new SQLQueryAdapter(sb.toString(), errors); - } } diff --git a/src/sqlancer/tidb/gen/TiDBIndexGenerator.java b/src/sqlancer/tidb/gen/TiDBIndexGenerator.java index 1be2753b6..64b4c808d 100644 --- a/src/sqlancer/tidb/gen/TiDBIndexGenerator.java +++ b/src/sqlancer/tidb/gen/TiDBIndexGenerator.java @@ -5,34 +5,39 @@ import sqlancer.IgnoreMeException; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.tidb.TiDBProvider.TiDBGlobalState; import sqlancer.tidb.TiDBSchema.TiDBColumn; import sqlancer.tidb.TiDBSchema.TiDBTable; -public final class TiDBIndexGenerator { +public class TiDBIndexGenerator extends AbstractIndexGenerator { - private TiDBIndexGenerator() { + private final TiDBGlobalState globalState; + + public TiDBIndexGenerator(TiDBGlobalState globalState) { + this.globalState = globalState; + this.canAffectSchema = true; } public static SQLQueryAdapter getQuery(TiDBGlobalState globalState) throws SQLException { if (globalState.getSchema().getIndexCount() > globalState.getDbmsSpecificOptions().maxNumIndexes) { throw new IgnoreMeException(); } - ExpectedErrors errors = new ExpectedErrors(); + return new TiDBIndexGenerator(globalState).getStatement(); + } + @Override + public void buildStatement() { TiDBTable randomTable = globalState.getSchema().getRandomTable(t -> !t.isView()); - String indexName = globalState.getSchema().getFreeIndexName(); - StringBuilder sb = new StringBuilder("CREATE "); - if (Randomly.getBooleanWithRatherLowProbability()) { - sb.append("UNIQUE "); + boolean unique = Randomly.getBooleanWithRatherLowProbability(); + if (unique) { errors.add("Duplicate for key"); errors.add("Duplicate entry "); errors.add("A UNIQUE INDEX must include all columns in the table's partitioning function"); } - sb.append("INDEX "); - sb.append(indexName); + appendCreateIndex(unique); + sb.append(globalState.getSchema().getFreeIndexName()); sb.append(" ON "); sb.append(randomTable.getName()); sb.append("("); @@ -63,7 +68,6 @@ public static SQLQueryAdapter getQuery(TiDBGlobalState globalState) throws SQLEx errors.add("index already exist"); errors.add("Data truncation"); errors.add("key was too long"); - return new SQLQueryAdapter(sb.toString(), errors, true); } } diff --git a/src/sqlancer/tidb/gen/TiDBUpdateGenerator.java b/src/sqlancer/tidb/gen/TiDBUpdateGenerator.java index 241ee3321..dd79670d9 100644 --- a/src/sqlancer/tidb/gen/TiDBUpdateGenerator.java +++ b/src/sqlancer/tidb/gen/TiDBUpdateGenerator.java @@ -1,6 +1,5 @@ package sqlancer.tidb.gen; -import java.sql.SQLException; import java.util.List; import sqlancer.Randomly; @@ -22,11 +21,12 @@ private TiDBUpdateGenerator(TiDBGlobalState globalState) { this.globalState = globalState; } - public static SQLQueryAdapter getQuery(TiDBGlobalState globalState) throws SQLException { - return new TiDBUpdateGenerator(globalState).generate(); + public static SQLQueryAdapter getQuery(TiDBGlobalState globalState) { + return new TiDBUpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() throws SQLException { + @Override + public void buildStatement() { TiDBTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new TiDBExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -35,13 +35,10 @@ private SQLQueryAdapter generate() throws SQLException { sb.append(" SET "); updateColumns(columns); if (Randomly.getBoolean()) { - sb.append(" WHERE "); TiDBErrors.addExpressionErrors(errors); - sb.append(TiDBVisitor.asString(gen.generateExpression())); + appendWhereClause(TiDBVisitor.asString(gen.generateExpression())); } TiDBErrors.addInsertErrors(errors); - - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/yugabyte/ycql/YCQLSchema.java b/src/sqlancer/yugabyte/ycql/YCQLSchema.java index 1534b9b56..736247364 100644 --- a/src/sqlancer/yugabyte/ycql/YCQLSchema.java +++ b/src/sqlancer/yugabyte/ycql/YCQLSchema.java @@ -126,6 +126,7 @@ public YCQLColumn(String name, YCQLCompositeDataType columnType, boolean isPrima this.isNullable = isNullable; } + @Override public boolean isPrimaryKey() { return isPrimaryKey; } @@ -216,7 +217,7 @@ public static YCQLSchema fromConnection(SQLConnection con, String databaseName) continue; } List databaseColumns = getTableColumns(con, databaseName, tableName); - boolean isView = tableName.startsWith("v"); + boolean isView = matchesViewName(tableName); YCQLTable t = new YCQLTable(tableName, databaseColumns, isView); for (YCQLColumn c : databaseColumns) { c.setTable(t); diff --git a/src/sqlancer/yugabyte/ycql/gen/YCQLDeleteGenerator.java b/src/sqlancer/yugabyte/ycql/gen/YCQLDeleteGenerator.java index 108cd1be9..af99ae4a5 100644 --- a/src/sqlancer/yugabyte/ycql/gen/YCQLDeleteGenerator.java +++ b/src/sqlancer/yugabyte/ycql/gen/YCQLDeleteGenerator.java @@ -1,31 +1,34 @@ package sqlancer.yugabyte.ycql.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.yugabyte.ycql.YCQLErrors; import sqlancer.yugabyte.ycql.YCQLProvider.YCQLGlobalState; import sqlancer.yugabyte.ycql.YCQLSchema.YCQLTable; import sqlancer.yugabyte.ycql.YCQLToStringVisitor; -public final class YCQLDeleteGenerator { +public final class YCQLDeleteGenerator extends AbstractDeleteGenerator { - private YCQLDeleteGenerator() { + private final YCQLGlobalState globalState; + + private YCQLDeleteGenerator(YCQLGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter generate(YCQLGlobalState globalState) { - StringBuilder sb = new StringBuilder("DELETE FROM "); - ExpectedErrors errors = new ExpectedErrors(); + return new YCQLDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { YCQLTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - sb.append(table.getName()); + appendDeleteFromTable(table.getName()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(YCQLToStringVisitor.asString( + appendWhereClause(YCQLToStringVisitor.asString( new YCQLExpressionGenerator(globalState).setColumns(table.getColumns()).generateExpression())); } - YCQLErrors.addExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/yugabyte/ycql/gen/YCQLIndexGenerator.java b/src/sqlancer/yugabyte/ycql/gen/YCQLIndexGenerator.java index ab03316cc..dd93b38d0 100644 --- a/src/sqlancer/yugabyte/ycql/gen/YCQLIndexGenerator.java +++ b/src/sqlancer/yugabyte/ycql/gen/YCQLIndexGenerator.java @@ -1,9 +1,7 @@ package sqlancer.yugabyte.ycql.gen; -import java.util.List; - import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.yugabyte.ycql.YCQLProvider.YCQLGlobalState; import sqlancer.yugabyte.ycql.YCQLSchema.YCQLColumn; @@ -11,45 +9,41 @@ import sqlancer.yugabyte.ycql.YCQLToStringVisitor; import sqlancer.yugabyte.ycql.ast.YCQLExpression; -public final class YCQLIndexGenerator { +public class YCQLIndexGenerator extends AbstractIndexGenerator { + + private final YCQLGlobalState globalState; - private YCQLIndexGenerator() { + public YCQLIndexGenerator(YCQLGlobalState globalState) { + this.globalState = globalState; + this.canAffectSchema = true; } public static SQLQueryAdapter getQuery(YCQLGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); - sb.append("CREATE "); - if (Randomly.getBoolean()) { + return new YCQLIndexGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { + boolean unique = Randomly.getBoolean(); + if (unique) { errors.add("Cant create unique index, table contains duplicate data on indexed column(s)"); - sb.append("UNIQUE "); } - sb.append("INDEX "); + appendCreateIndex(unique); sb.append(Randomly.fromOptions("i0", "i1", "i2", "i3", "i4")); sb.append(" ON "); YCQLTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); sb.append(table.getName()); - sb.append("("); - List columns = table.getRandomNonEmptyColumnSubset(); - for (int i = 0; i < columns.size(); i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(columns.get(i).getName()); - } - sb.append(")"); + appendIndexColumnList(table.getRandomNonEmptyColumnSubset(), false); if (Randomly.getBoolean()) { - sb.append(" WHERE "); YCQLExpression expr = new YCQLExpressionGenerator(globalState).setColumns(table.getColumns()) .generateExpression(); - sb.append(YCQLToStringVisitor.asString(expr)); + appendWhereClause(YCQLToStringVisitor.asString(expr)); } errors.add("Query timed out after PT2S"); errors.add("Invalid SQL Statement"); errors.add("Invalid CQL Statement"); errors.add( "Invalid Table Definition. Transactions cannot be enabled in an index of a table without transactions enabled."); - return new SQLQueryAdapter(sb.toString(), errors, true); } } diff --git a/src/sqlancer/yugabyte/ycql/gen/YCQLInsertGenerator.java b/src/sqlancer/yugabyte/ycql/gen/YCQLInsertGenerator.java index a1159d310..167f5d237 100644 --- a/src/sqlancer/yugabyte/ycql/gen/YCQLInsertGenerator.java +++ b/src/sqlancer/yugabyte/ycql/gen/YCQLInsertGenerator.java @@ -1,12 +1,9 @@ package sqlancer.yugabyte.ycql.gen; import java.util.List; -import java.util.stream.Collectors; import sqlancer.common.gen.AbstractInsertGenerator; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLQueryAdapter; -import sqlancer.common.schema.AbstractTableColumn; import sqlancer.yugabyte.ycql.YCQLErrors; import sqlancer.yugabyte.ycql.YCQLProvider.YCQLGlobalState; import sqlancer.yugabyte.ycql.YCQLSchema.YCQLColumn; @@ -16,32 +13,25 @@ public class YCQLInsertGenerator extends AbstractInsertGenerator { private final YCQLGlobalState globalState; - private final ExpectedErrors errors = new ExpectedErrors(); public YCQLInsertGenerator(YCQLGlobalState globalState) { this.globalState = globalState; } public static SQLQueryAdapter getQuery(YCQLGlobalState globalState) { - return new YCQLInsertGenerator(globalState).generate(); + return new YCQLInsertGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { - sb.append("INSERT INTO "); + @Override + public void buildStatement() { YCQLTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getColumns(); - sb.append(globalState.getDatabaseName()).append(".").append(table.getName()); - sb.append("("); - sb.append(columns.stream().map(AbstractTableColumn::getName).collect(Collectors.joining(", "))); - sb.append(")"); - sb.append(" VALUES "); - insertColumns(columns); + buildInsertInto(globalState.getDatabaseName() + "." + table.getName(), columns); errors.add("Invalid Arguments"); errors.add("Null Argument for Primary Key"); YCQLErrors.addExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/yugabyte/ycql/gen/YCQLTableGenerator.java b/src/sqlancer/yugabyte/ycql/gen/YCQLTableGenerator.java index 148a1cdbc..c57f94287 100644 --- a/src/sqlancer/yugabyte/ycql/gen/YCQLTableGenerator.java +++ b/src/sqlancer/yugabyte/ycql/gen/YCQLTableGenerator.java @@ -5,35 +5,33 @@ import java.util.stream.Collectors; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractTableGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.common.schema.AbstractTableColumn; import sqlancer.yugabyte.ycql.YCQLProvider.YCQLGlobalState; import sqlancer.yugabyte.ycql.YCQLSchema.YCQLColumn; import sqlancer.yugabyte.ycql.YCQLSchema.YCQLCompositeDataType; -public class YCQLTableGenerator { +public class YCQLTableGenerator extends AbstractTableGenerator { + + private YCQLGlobalState globalState; + + public YCQLTableGenerator() { + this.canAffectSchema = true; + } public SQLQueryAdapter getQuery(YCQLGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); + this.globalState = globalState; + return getStatement(); + } + + @Override + public void buildStatement() { String tableName = globalState.getSchema().getFreeTableName(); - sb.append("CREATE TABLE "); - if (Randomly.getBoolean()) { - sb.append("IF NOT EXISTS "); - } - sb.append(tableName); - sb.append("("); + appendCreateTable(tableName, Randomly.getBoolean()); List columns = getNewColumns(); - for (int i = 0; i < columns.size(); i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(columns.get(i).getName()); - sb.append(" "); - sb.append(columns.get(i).getType()); - // todo PK, STATIC - } + sb.append("("); + appendColumnDefinitionList(columns); errors.add("Query timed out after PT2S"); errors.add("Invalid type for index"); List primaryKeyColumns = Randomly.nonEmptySubset(columns); @@ -41,7 +39,6 @@ public SQLQueryAdapter getQuery(YCQLGlobalState globalState) { sb.append(primaryKeyColumns.stream().map(AbstractTableColumn::getName).collect(Collectors.joining(", "))); sb.append(")"); sb.append(")"); - return new SQLQueryAdapter(sb.toString(), errors, true); } private static List getNewColumns() { diff --git a/src/sqlancer/yugabyte/ycql/gen/YCQLUpdateGenerator.java b/src/sqlancer/yugabyte/ycql/gen/YCQLUpdateGenerator.java index a6c855cf3..eee49bee4 100644 --- a/src/sqlancer/yugabyte/ycql/gen/YCQLUpdateGenerator.java +++ b/src/sqlancer/yugabyte/ycql/gen/YCQLUpdateGenerator.java @@ -22,10 +22,11 @@ private YCQLUpdateGenerator(YCQLGlobalState globalState) { } public static SQLQueryAdapter getQuery(YCQLGlobalState globalState) { - return new YCQLUpdateGenerator(globalState).generate(); + return new YCQLUpdateGenerator(globalState).getStatement(); } - private SQLQueryAdapter generate() { + @Override + public void buildStatement() { YCQLTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); List columns = table.getRandomNonEmptyColumnSubset(); gen = new YCQLExpressionGenerator(globalState).setColumns(table.getColumns()); @@ -41,7 +42,6 @@ private SQLQueryAdapter generate() { errors.add("Missing Argument for Primary Key"); YCQLErrors.addExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } @Override diff --git a/src/sqlancer/yugabyte/ysql/YSQLProvider.java b/src/sqlancer/yugabyte/ysql/YSQLProvider.java index e72790344..efcc7ec22 100644 --- a/src/sqlancer/yugabyte/ysql/YSQLProvider.java +++ b/src/sqlancer/yugabyte/ysql/YSQLProvider.java @@ -204,11 +204,11 @@ private void createDatabaseSync(YSQLGlobalState globalState, String entryDatabas Connection con = createConnectionSafely(entryURL, username, password); globalState.getState().logStatement(String.format("\\c %s;", entryDatabaseName)); - globalState.getState().logStatement("DROP DATABASE IF EXISTS " + databaseName); + globalState.getState().logStatement("DROP DATABASE IF EXISTS " + databaseName + " WITH (FORCE)"); createDatabaseCommand = getCreateDatabaseCommand(globalState); globalState.getState().logStatement(createDatabaseCommand); try (Statement s = con.createStatement()) { - s.execute("DROP DATABASE IF EXISTS " + databaseName); + s.execute("DROP DATABASE IF EXISTS " + databaseName + " WITH (FORCE)"); } try (Statement s = con.createStatement()) { s.execute(createDatabaseCommand); diff --git a/src/sqlancer/yugabyte/ysql/YSQLSchema.java b/src/sqlancer/yugabyte/ysql/YSQLSchema.java index 400a34e21..c75322af9 100644 --- a/src/sqlancer/yugabyte/ysql/YSQLSchema.java +++ b/src/sqlancer/yugabyte/ysql/YSQLSchema.java @@ -81,7 +81,7 @@ public static YSQLSchema fromConnection(SQLConnection con, String databaseName) boolean isInsertable = rs.getBoolean("is_insertable_into"); // TODO: also check insertable // TODO: insert into view? - boolean isView = tableName.startsWith("v"); // tableTypeStr.contains("VIEW") || + boolean isView = matchesViewName(tableName); // tableTypeStr.contains("VIEW") || // tableTypeStr.contains("LOCAL TEMPORARY") && // !isInsertable; YSQLTable.TableType tableType = getTableType(tableTypeSchema); diff --git a/src/sqlancer/yugabyte/ysql/gen/YSQLDeleteGenerator.java b/src/sqlancer/yugabyte/ysql/gen/YSQLDeleteGenerator.java index e0128707b..9d3c7427d 100644 --- a/src/sqlancer/yugabyte/ysql/gen/YSQLDeleteGenerator.java +++ b/src/sqlancer/yugabyte/ysql/gen/YSQLDeleteGenerator.java @@ -1,7 +1,7 @@ package sqlancer.yugabyte.ysql.gen; import sqlancer.Randomly; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractDeleteGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.yugabyte.ysql.YSQLErrors; import sqlancer.yugabyte.ysql.YSQLGlobalState; @@ -9,31 +9,31 @@ import sqlancer.yugabyte.ysql.YSQLSchema.YSQLTable; import sqlancer.yugabyte.ysql.YSQLVisitor; -public final class YSQLDeleteGenerator { +public final class YSQLDeleteGenerator extends AbstractDeleteGenerator { - private YSQLDeleteGenerator() { + private final YSQLGlobalState globalState; + + private YSQLDeleteGenerator(YSQLGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter create(YSQLGlobalState globalState) { + return new YSQLDeleteGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { YSQLTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); - ExpectedErrors errors = new ExpectedErrors(); errors.add("violates foreign key constraint"); errors.add("violates not-null constraint"); errors.add("could not determine which collation to use for string comparison"); - StringBuilder sb = new StringBuilder("DELETE FROM"); - if (Randomly.getBoolean()) { - sb.append(" ONLY"); - } - sb.append(" "); - sb.append(table.getName()); + appendDeleteFromTable(table.getName(), Randomly.getBoolean()); if (Randomly.getBoolean()) { - sb.append(" WHERE "); - sb.append(YSQLVisitor.asString( + appendWhereClause(YSQLVisitor.asString( YSQLExpressionGenerator.generateExpression(globalState, table.getColumns(), YSQLDataType.BOOLEAN))); } if (Randomly.getBoolean()) { - sb.append(" RETURNING "); - sb.append( + appendReturningClause( YSQLVisitor.asString(YSQLExpressionGenerator.generateExpression(globalState, table.getColumns()))); } YSQLErrors.addCommonExpressionErrors(errors); @@ -41,7 +41,6 @@ public static SQLQueryAdapter create(YSQLGlobalState globalState) { errors.add("cannot cast"); errors.add("invalid input syntax for"); errors.add("division by zero"); - return new SQLQueryAdapter(sb.toString(), errors); } } diff --git a/src/sqlancer/yugabyte/ysql/gen/YSQLIndexGenerator.java b/src/sqlancer/yugabyte/ysql/gen/YSQLIndexGenerator.java index 6077dcb1e..5453746b2 100644 --- a/src/sqlancer/yugabyte/ysql/gen/YSQLIndexGenerator.java +++ b/src/sqlancer/yugabyte/ysql/gen/YSQLIndexGenerator.java @@ -5,7 +5,7 @@ import sqlancer.Randomly; import sqlancer.common.DBMSCommon; -import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.gen.AbstractIndexGenerator; import sqlancer.common.query.SQLQueryAdapter; import sqlancer.common.schema.AbstractTableColumn; import sqlancer.yugabyte.ysql.YSQLErrors; @@ -17,19 +17,21 @@ import sqlancer.yugabyte.ysql.YSQLVisitor; import sqlancer.yugabyte.ysql.ast.YSQLExpression; -public final class YSQLIndexGenerator { +public class YSQLIndexGenerator extends AbstractIndexGenerator { - private YSQLIndexGenerator() { + private final YSQLGlobalState globalState; + + public YSQLIndexGenerator(YSQLGlobalState globalState) { + this.globalState = globalState; } public static SQLQueryAdapter generate(YSQLGlobalState globalState) { - ExpectedErrors errors = new ExpectedErrors(); - StringBuilder sb = new StringBuilder(); - sb.append("CREATE"); - if (Randomly.getBoolean()) { - sb.append(" UNIQUE"); - } - sb.append(" INDEX "); + return new YSQLIndexGenerator(globalState).getStatement(); + } + + @Override + public void buildStatement() { + appendCreateIndex(Randomly.getBoolean()); YSQLTable randomTable = globalState.getSchema().getRandomTable(t -> !t.isView()); // TODO: materialized // views String indexName = getNewIndexName(randomTable); @@ -91,10 +93,9 @@ public static SQLQueryAdapter generate(YSQLGlobalState globalState) { sb.append(")"); } if (Randomly.getBoolean()) { - sb.append(" WHERE "); YSQLExpression expr = new YSQLExpressionGenerator(globalState).setColumns(randomTable.getColumns()) .setGlobalState(globalState).generateExpression(YSQLDataType.BOOLEAN); - sb.append(YSQLVisitor.asString(expr)); + appendWhereClause(YSQLVisitor.asString(expr)); } errors.add("already contains data"); // CONCURRENT INDEX failed errors.add("You might need to add explicit type casts"); @@ -122,7 +123,6 @@ public static SQLQueryAdapter generate(YSQLGlobalState globalState) { errors.add("result of range difference would not be contiguous"); errors.add("which is part of the partition key"); YSQLErrors.addCommonExpressionErrors(errors); - return new SQLQueryAdapter(sb.toString(), errors); } private static String getNewIndexName(YSQLTable randomTable) { diff --git a/src/sqlancer/yugabyte/ysql/gen/YSQLUpdateGenerator.java b/src/sqlancer/yugabyte/ysql/gen/YSQLUpdateGenerator.java index bc7b00d79..1a9e1de76 100644 --- a/src/sqlancer/yugabyte/ysql/gen/YSQLUpdateGenerator.java +++ b/src/sqlancer/yugabyte/ysql/gen/YSQLUpdateGenerator.java @@ -21,6 +21,7 @@ public final class YSQLUpdateGenerator extends AbstractUpdateGenerator columns = randomTable.getRandomNonEmptyColumnSubset(); sb.append("UPDATE "); @@ -52,13 +54,10 @@ private SQLQueryAdapter generate() { errors.add("but expression is of type"); YSQLErrors.addCommonExpressionErrors(errors); if (!Randomly.getBooleanWithSmallProbability()) { - sb.append(" WHERE "); YSQLExpression where = YSQLExpressionGenerator.generateExpression(globalState, randomTable.getColumns(), YSQLDataType.BOOLEAN); - sb.append(YSQLVisitor.asString(where)); + appendWhereClause(YSQLVisitor.asString(where)); } - - return new SQLQueryAdapter(sb.toString(), errors, true); } @Override diff --git a/src/sqlancer/yugabyte/ysql/gen/YSQLViewGenerator.java b/src/sqlancer/yugabyte/ysql/gen/YSQLViewGenerator.java index f70891cc2..3005d49f8 100644 --- a/src/sqlancer/yugabyte/ysql/gen/YSQLViewGenerator.java +++ b/src/sqlancer/yugabyte/ysql/gen/YSQLViewGenerator.java @@ -28,19 +28,11 @@ public static SQLQueryAdapter create(YSQLGlobalState globalState) { } } sb.append(" VIEW "); - int i = 0; - String[] name = new String[1]; - while (true) { - name[0] = "v" + i++; - if (globalState.getSchema().getDatabaseTables().stream() - .noneMatch(tab -> tab.getName().contentEquals(name[0]))) { - break; - } - } - sb.append(name[0]); + String name = globalState.getSchema().getFreeViewName(); + sb.append(name); sb.append("("); int nrColumns = Randomly.smallNumber() + 1; - for (i = 0; i < nrColumns; i++) { + for (int i = 0; i < nrColumns; i++) { if (i != 0) { sb.append(", "); } diff --git a/test/sqlancer/dbms/TestCnosDBNoREC.java b/test/sqlancer/dbms/TestCnosDBNoREC.java deleted file mode 100644 index 1a89a972a..000000000 --- a/test/sqlancer/dbms/TestCnosDBNoREC.java +++ /dev/null @@ -1,22 +0,0 @@ -package sqlancer.dbms; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -import org.junit.jupiter.api.Test; - -import sqlancer.Main; - -public class TestCnosDBNoREC { - - @Test - public void testCnosDBNoREC() { - assumeTrue(TestConfig.isEnvironmentTrue(TestConfig.CNOSDB_ENV)); - // Run with 0 queries as current implementation is resulting in database crashes - assertEquals(0, - Main.executeMain(new String[] { "--host", "127.0.0.1", "--port", "8902", "--username", "root", - "--random-seed", "0", "--timeout-seconds", TestConfig.SECONDS, "--num-queries", "0", "cnosdb", - "--oracle", "NOREC" })); - } - -} diff --git a/test/sqlancer/dbms/TestCnosDBTLP.java b/test/sqlancer/dbms/TestCnosDBTLP.java deleted file mode 100644 index 4b12aa409..000000000 --- a/test/sqlancer/dbms/TestCnosDBTLP.java +++ /dev/null @@ -1,22 +0,0 @@ -package sqlancer.dbms; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -import org.junit.jupiter.api.Test; - -import sqlancer.Main; - -public class TestCnosDBTLP { - - @Test - public void testCnosDBTLP() { - assumeTrue(TestConfig.isEnvironmentTrue(TestConfig.CNOSDB_ENV)); - // Run with 0 queries as current implementation is resulting in database crashes - assertEquals(0, - Main.executeMain(new String[] { "--host", "127.0.0.1", "--port", "8902", "--username", "root", - "--random-seed", "0", "--timeout-seconds", TestConfig.SECONDS, "--num-queries", "0", "cnosdb", - "--oracle", "QUERY_PARTITIONING" })); - } - -} diff --git a/test/sqlancer/dbms/TestConfig.java b/test/sqlancer/dbms/TestConfig.java index f5aeefa12..f2372f266 100644 --- a/test/sqlancer/dbms/TestConfig.java +++ b/test/sqlancer/dbms/TestConfig.java @@ -5,12 +5,12 @@ public class TestConfig { public static final String SECONDS = "300"; public static final String CLICKHOUSE_ENV = "CLICKHOUSE_AVAILABLE"; - public static final String CNOSDB_ENV = "CNOSDB_AVAILABLE"; public static final String COCKROACHDB_ENV = "COCKROACHDB_AVAILABLE"; public static final String DATABEND_ENV = "DATABEND_AVAILABLE"; public static final String DATAFUSION_ENV = "DATAFUSION_AVAILABLE"; public static final String DORIS_ENV = "DORIS_AVAILABLE"; public static final String HIVE_ENV = "HIVE_AVAILABLE"; + public static final String SPARK_ENV = "SPARK_AVAILABLE"; public static final String MARIADB_ENV = "MARIADB_AVAILABLE"; public static final String MATERIALIZE_ENV = "MATERIALIZE_AVAILABLE"; public static final String MYSQL_ENV = "MYSQL_AVAILABLE"; diff --git a/test/sqlancer/dbms/TestSparkTLP.java b/test/sqlancer/dbms/TestSparkTLP.java new file mode 100644 index 000000000..83302ceff --- /dev/null +++ b/test/sqlancer/dbms/TestSparkTLP.java @@ -0,0 +1,20 @@ +package sqlancer.dbms; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.Test; + +import sqlancer.Main; + +public class TestSparkTLP { + + @Test + public void testSparkTLPWhere() { + assumeTrue(TestConfig.isEnvironmentTrue(TestConfig.SPARK_ENV)); + assertEquals(0, + Main.executeMain(new String[] { "--canonicalize-sql-strings", "false", "--random-seed", "0", + "--timeout-seconds", TestConfig.SECONDS, "--num-threads", "1", "--num-queries", + TestConfig.NUM_QUERIES, "spark", "--oracle", "TLPWhere" })); + } +} \ No newline at end of file diff --git a/test/sqlancer/mysql/MySQLToStringVisitorTest.java b/test/sqlancer/mysql/MySQLToStringVisitorTest.java index 3f991e3f1..d39c2695c 100644 --- a/test/sqlancer/mysql/MySQLToStringVisitorTest.java +++ b/test/sqlancer/mysql/MySQLToStringVisitorTest.java @@ -17,7 +17,7 @@ public class MySQLToStringVisitorTest { @Test void visitAggregateToString() { - MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0); + MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0, 0); MySQLColumnReference aRef = new MySQLColumnReference(aCol, MySQLConstant.createNullConstant()); MySQLAggregate aggrCount = new MySQLAggregate(List.of(aRef), MySQLAggregate.MySQLAggregateFunction.COUNT); @@ -35,7 +35,7 @@ void visitAggregateToString() { @Test void visitAggregateWithDistinctToString() { - MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0); + MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0, 0); MySQLColumnReference aRef = new MySQLColumnReference(aCol, MySQLConstant.createNullConstant()); MySQLAggregate aggrCountDistinct = new MySQLAggregate(List.of(aRef), @@ -57,7 +57,7 @@ void visitAggregateWithDistinctToString() { @Test void visitCaseWhenToString() { - MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0); + MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0, 0); MySQLColumnReference switchExpr = new MySQLColumnReference(aCol, MySQLConstant.createNullConstant()); List whenExprs = List.of(MySQLIntConstant.createIntConstant(1), MySQLIntConstant.createIntConstant(2)); diff --git a/test/sqlancer/mysql/ast/MySQLCaseOperatorTest.java b/test/sqlancer/mysql/ast/MySQLCaseOperatorTest.java index 674ab027e..757241445 100644 --- a/test/sqlancer/mysql/ast/MySQLCaseOperatorTest.java +++ b/test/sqlancer/mysql/ast/MySQLCaseOperatorTest.java @@ -14,7 +14,7 @@ public class MySQLCaseOperatorTest { @Test void getExpectedValue_switchConditionMatchesWhen_ReturnsThen() { - MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0); + MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0, 0); MySQLColumnReference switchExpr = new MySQLColumnReference(aCol, MySQLIntConstant.createIntConstant(1)); List whenExprs = List.of(MySQLIntConstant.createIntConstant(1), MySQLIntConstant.createIntConstant(2)); @@ -29,7 +29,7 @@ void getExpectedValue_switchConditionMatchesWhen_ReturnsThen() { @Test void getExpectedValue_switchConditionHasNoMatches_ReturnsElse() { - MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0); + MySQLSchema.MySQLColumn aCol = new MySQLSchema.MySQLColumn("a", MySQLSchema.MySQLDataType.INT, false, 0, 0); MySQLColumnReference switchExpr = new MySQLColumnReference(aCol, MySQLIntConstant.createNullConstant()); List whenExprs = List.of(MySQLIntConstant.createIntConstant(1), MySQLIntConstant.createIntConstant(2)); diff --git a/test/sqlancer/postgres/TestPostgresProvider.java b/test/sqlancer/postgres/TestPostgresProvider.java new file mode 100644 index 000000000..81e795568 --- /dev/null +++ b/test/sqlancer/postgres/TestPostgresProvider.java @@ -0,0 +1,21 @@ +package sqlancer.postgres; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class TestPostgresProvider { + + @Test + void createTablespaceIsNotScheduledWhenDisabled() { + PostgresGlobalState state = new PostgresGlobalState(); + state.setDbmsSpecificOptions(new PostgresOptions() { + @Override + public boolean isTestTablespaces() { + return false; + } + }); + + assertEquals(0, PostgresProvider.mapActions(state, PostgresProvider.Action.CREATE_TABLESPACE)); + } +} diff --git a/test/sqlancer/postgres/gen/TestPostgresTableSpaceGenerator.java b/test/sqlancer/postgres/gen/TestPostgresTableSpaceGenerator.java new file mode 100644 index 000000000..c2f626041 --- /dev/null +++ b/test/sqlancer/postgres/gen/TestPostgresTableSpaceGenerator.java @@ -0,0 +1,25 @@ +package sqlancer.postgres.gen; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import sqlancer.IgnoreMeException; +import sqlancer.postgres.PostgresGlobalState; +import sqlancer.postgres.PostgresOptions; + +class TestPostgresTableSpaceGenerator { + + @Test + void generateIsSkippedWhenTablespacesAreDisabled() { + PostgresGlobalState state = new PostgresGlobalState(); + state.setDbmsSpecificOptions(new PostgresOptions() { + @Override + public boolean isTestTablespaces() { + return false; + } + }); + + assertThrows(IgnoreMeException.class, () -> PostgresTableSpaceGenerator.generate(state)); + } +} diff --git a/test/sqlancer/qpg/materialize/TestMaterializeQueryPlan.java b/test/sqlancer/qpg/materialize/TestMaterializeQueryPlan.java index f282af42e..4d26bc08e 100644 --- a/test/sqlancer/qpg/materialize/TestMaterializeQueryPlan.java +++ b/test/sqlancer/qpg/materialize/TestMaterializeQueryPlan.java @@ -42,7 +42,7 @@ void testMaterializeQueryPlan() throws Exception { String queryPlan = provider.getQueryPlan("SELECT * FROM t1 RIGHT JOIN t2 ON a<>0;", state); assertEquals( - "Return // { arity: 3 };Union // { arity: 3 };Get l0 // { arity: 3 };Project (#2{c}, #3, #0) // { arity: 3 };Union // { arity: 1 };Negate // { arity: 1 };Project (#2) // { arity: 1 };ReadStorage queryplan.public.t2 // { arity: 1 };ReadStorage queryplan.public.t2 // { arity: 1 };With;ReadStorage queryplan.public.t1 // { arity: 2 };ReadStorage queryplan.public.t2 // { arity: 1 };;Source queryplan.public.t1;Source queryplan.public.t2;;Target cluster: quickstart;", + "With;ReadStorage queryplan.public.t1 // { arity: 2 };ReadStorage queryplan.public.t2 // { arity: 1 };Return // { arity: 3 };Union // { arity: 3 };Get l0 // { arity: 3 };Project (#2, #3, #0{c}) // { arity: 3 };Union // { arity: 1 };Negate // { arity: 1 };Project (#2{c}) // { arity: 1 };ReadStorage queryplan.public.t2 // { arity: 1 };ReadStorage queryplan.public.t2 // { arity: 1 };;Source queryplan.public.t1;Source queryplan.public.t2;;Target cluster: quickstart;", queryPlan); }