From 38f1972d41e68d254774b8d1f42b8c26c1920ad0 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 3 Jan 2021 11:39:42 +0100 Subject: [PATCH 01/24] Introduce MongoDB to SQLancer and support creating collections Adding support for mongodb database version 4.4. With this commit random collections are created with optional validation options which can enforce the schema on inserts. Additionally the execution is logged in mongo shell code. --- pom.xml | 5 + src/sqlancer/GlobalState.java | 6 +- src/sqlancer/Main.java | 4 +- .../TernaryLogicPartitioningOracleBase.java | 4 +- src/sqlancer/mongodb/MongoDBConnection.java | 31 +++++ .../mongodb/MongoDBLoggableFactory.java | 40 ++++++ src/sqlancer/mongodb/MongoDBOptions.java | 40 ++++++ src/sqlancer/mongodb/MongoDBProvider.java | 107 ++++++++++++++++ src/sqlancer/mongodb/MongoDBQueryAdapter.java | 21 ++++ .../mongodb/MongoDBQueryProvider.java | 6 + src/sqlancer/mongodb/MongoDBSchema.java | 117 ++++++++++++++++++ .../mongodb/ast/MongoDBExpression.java | 4 + .../mongodb/gen/MongoDBTableGenerator.java | 49 ++++++++ .../query/MongoDBCreateTableQuery.java | 109 ++++++++++++++++ .../test/MongoDBQueryPartitioningBase.java | 26 ++++ .../MongoDBQueryPartitioningWhereTester.java | 9 ++ 16 files changed, 572 insertions(+), 6 deletions(-) create mode 100644 src/sqlancer/mongodb/MongoDBConnection.java create mode 100644 src/sqlancer/mongodb/MongoDBLoggableFactory.java create mode 100644 src/sqlancer/mongodb/MongoDBOptions.java create mode 100644 src/sqlancer/mongodb/MongoDBProvider.java create mode 100644 src/sqlancer/mongodb/MongoDBQueryAdapter.java create mode 100644 src/sqlancer/mongodb/MongoDBQueryProvider.java create mode 100644 src/sqlancer/mongodb/MongoDBSchema.java create mode 100644 src/sqlancer/mongodb/ast/MongoDBExpression.java create mode 100644 src/sqlancer/mongodb/gen/MongoDBTableGenerator.java create mode 100644 src/sqlancer/mongodb/query/MongoDBCreateTableQuery.java create mode 100644 src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java create mode 100644 src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java diff --git a/pom.xml b/pom.xml index 3dea1ef15..c0f0bbd9b 100644 --- a/pom.xml +++ b/pom.xml @@ -266,6 +266,11 @@ h2 1.4.200 + + org.mongodb + mongodb-driver-sync + 4.1.1 + diff --git a/src/sqlancer/GlobalState.java b/src/sqlancer/GlobalState.java index 97c5a7a9d..642c9da6a 100644 --- a/src/sqlancer/GlobalState.java +++ b/src/sqlancer/GlobalState.java @@ -89,13 +89,13 @@ private ExecutionTimer executePrologue(Query q) throws Exception { timer = new ExecutionTimer().start(); } if (getOptions().printAllStatements()) { - System.out.println(q.getQueryString()); + System.out.println(q.getLogString()); } if (getOptions().logEachSelect()) { if (logExecutionTime) { - getLogger().writeCurrentNoLineBreak(q.getQueryString()); + getLogger().writeCurrentNoLineBreak(q.getLogString()); } else { - getLogger().writeCurrent(q.getQueryString()); + getLogger().writeCurrent(q.getLogString()); } } return timer; diff --git a/src/sqlancer/Main.java b/src/sqlancer/Main.java index 755ce36a8..d606ee0f5 100644 --- a/src/sqlancer/Main.java +++ b/src/sqlancer/Main.java @@ -30,6 +30,7 @@ import sqlancer.duckdb.DuckDBProvider; import sqlancer.h2.H2Provider; import sqlancer.mariadb.MariaDBProvider; +import sqlancer.mongodb.MongoDBProvider; import sqlancer.mysql.MySQLProvider; import sqlancer.postgres.PostgresProvider; import sqlancer.sqlite3.SQLite3Provider; @@ -209,7 +210,7 @@ private void printState(FileWriter writer, StateToReproduce state) { .getInfo(state.getDatabaseName(), state.getDatabaseVersion(), state.getSeedValue()).getLogString()); for (Query s : state.getStatements()) { - sb.append(s.getQueryString()); + sb.append(s.getLogString()); sb.append('\n'); } try { @@ -554,6 +555,7 @@ private boolean run(MainOptions options, ExecutorService execService, providers.add(new ClickHouseProvider()); providers.add(new DuckDBProvider()); providers.add(new H2Provider()); + providers.add(new MongoDBProvider()); return providers; } diff --git a/src/sqlancer/common/oracle/TernaryLogicPartitioningOracleBase.java b/src/sqlancer/common/oracle/TernaryLogicPartitioningOracleBase.java index 991824628..3b5d87814 100644 --- a/src/sqlancer/common/oracle/TernaryLogicPartitioningOracleBase.java +++ b/src/sqlancer/common/oracle/TernaryLogicPartitioningOracleBase.java @@ -1,6 +1,6 @@ package sqlancer.common.oracle; -import sqlancer.SQLGlobalState; +import sqlancer.GlobalState; import sqlancer.common.gen.ExpressionGenerator; import sqlancer.common.query.ExpectedErrors; @@ -14,7 +14,7 @@ * @param * the global state type */ -public abstract class TernaryLogicPartitioningOracleBase> implements TestOracle { +public abstract class TernaryLogicPartitioningOracleBase> implements TestOracle { protected E predicate; protected E negatedPredicate; diff --git a/src/sqlancer/mongodb/MongoDBConnection.java b/src/sqlancer/mongodb/MongoDBConnection.java new file mode 100644 index 000000000..c15a46be5 --- /dev/null +++ b/src/sqlancer/mongodb/MongoDBConnection.java @@ -0,0 +1,31 @@ +package sqlancer.mongodb; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoDatabase; + +import sqlancer.SQLancerDBConnection; + +public class MongoDBConnection implements SQLancerDBConnection { + + private final MongoClient client; + private final MongoDatabase database; + + public MongoDBConnection(MongoClient client, MongoDatabase database) { + this.client = client; + this.database = database; + } + + @Override + public String getDatabaseVersion() throws Exception { + return "4.4, Java API 4.1"; + } + + @Override + public void close() throws Exception { + client.close(); + } + + public MongoDatabase getDatabase() { + return database; + } +} diff --git a/src/sqlancer/mongodb/MongoDBLoggableFactory.java b/src/sqlancer/mongodb/MongoDBLoggableFactory.java new file mode 100644 index 000000000..b668301b3 --- /dev/null +++ b/src/sqlancer/mongodb/MongoDBLoggableFactory.java @@ -0,0 +1,40 @@ +package sqlancer.mongodb; + +import java.util.Arrays; + +import sqlancer.common.log.Loggable; +import sqlancer.common.log.LoggableFactory; +import sqlancer.common.log.LoggedString; +import sqlancer.common.query.Query; + +public class MongoDBLoggableFactory extends LoggableFactory { + @Override + protected Loggable createLoggable(String input, String suffix) { + return new LoggedString(input + suffix); + } + + @Override + public Query getQueryForStateToReproduce(String queryString) { + throw new UnsupportedOperationException(); + } + + @Override + public Query commentOutQuery(Query query) { + throw new UnsupportedOperationException(); + } + + @Override + protected Loggable infoToLoggable(String time, String databaseName, String databaseVersion, long seedValue) { + StringBuilder sb = new StringBuilder(); + sb.append("// Time: ").append(time).append("\n"); + sb.append("// Database: ").append(databaseName).append("\n"); + sb.append("// Database version: ").append(databaseVersion).append("\n"); + sb.append("// seed value: ").append(seedValue).append("\n"); + return new LoggedString(sb.toString()); + } + + @Override + public Loggable convertStacktraceToLoggable(Throwable throwable) { + return new LoggedString(Arrays.toString(throwable.getStackTrace()) + "\n" + throwable.getMessage()); + } +} diff --git a/src/sqlancer/mongodb/MongoDBOptions.java b/src/sqlancer/mongodb/MongoDBOptions.java new file mode 100644 index 000000000..58086ac2f --- /dev/null +++ b/src/sqlancer/mongodb/MongoDBOptions.java @@ -0,0 +1,40 @@ +package sqlancer.mongodb; + +import static sqlancer.mongodb.MongoDBOptions.MongoDBOracleFactory.QUERY_PARTITIONING; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.beust.jcommander.Parameter; + +import sqlancer.DBMSSpecificOptions; +import sqlancer.OracleFactory; +import sqlancer.common.oracle.CompositeTestOracle; +import sqlancer.common.oracle.TestOracle; +import sqlancer.mongodb.test.MongoDBQueryPartitioningWhereTester; + +public class MongoDBOptions implements DBMSSpecificOptions { + + @Parameter(names = "--test-validation", description = "Enable/Disable validation of schema with Schema Validation", arity = 1) + public boolean testValidation = true; + + @Parameter(names = "--oracle") + public List oracles = Arrays.asList(QUERY_PARTITIONING); + + @Override + public List getTestOracleFactory() { + return oracles; + } + + public enum MongoDBOracleFactory implements OracleFactory { + QUERY_PARTITIONING { + @Override + public TestOracle create(MongoDBProvider.MongoDBGlobalState globalState) throws Exception { + List oracles = new ArrayList<>(); + oracles.add(new MongoDBQueryPartitioningWhereTester(globalState)); + return new CompositeTestOracle(oracles, globalState); + } + } + } +} diff --git a/src/sqlancer/mongodb/MongoDBProvider.java b/src/sqlancer/mongodb/MongoDBProvider.java new file mode 100644 index 000000000..5fd4801a0 --- /dev/null +++ b/src/sqlancer/mongodb/MongoDBProvider.java @@ -0,0 +1,107 @@ +package sqlancer.mongodb; + +import java.util.ArrayList; +import java.util.List; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; + +import sqlancer.ExecutionTimer; +import sqlancer.GlobalState; +import sqlancer.ProviderAdapter; +import sqlancer.Randomly; +import sqlancer.common.log.LoggableFactory; +import sqlancer.common.query.Query; +import sqlancer.mongodb.MongoDBSchema.MongoDBTable; +import sqlancer.mongodb.gen.MongoDBTableGenerator; + +public class MongoDBProvider + extends ProviderAdapter { + + public MongoDBProvider() { + super(MongoDBGlobalState.class, MongoDBOptions.class); + } + + /* + * enum Action implements AbstractAction { ; INSERT(MongoDBInsertGenerator::getQuery); + * CREATE_INDEX(MongoDBIndexGenerator::getQuery); + * + * private final MongoDBQueryProvider queryProvider; + * + * Action(MongoDBQueryProvider queryProvider) { this.queryProvider = queryProvider; } + * + * @Override public Query getQuery(MongoDBGlobalState globalState) throws Exception { return + * queryProvider.getQuery(globalState); } } + * + * private static int mapActions(MongoDBGlobalState globalState, Action a) { Randomly r = globalState.getRandomly(); + * switch (a) { case INSERT: return r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); case + * CREATE_INDEX: return r.getInteger(0, 2); default: throw new AssertionError(a); } return -1; } + */ + + public static class MongoDBGlobalState extends GlobalState { + + private final List schemaTables = new ArrayList<>(); + + public synchronized void addTable(MongoDBTable table) { + schemaTables.add(table); + } + + @Override + protected void executeEpilogue(Query q, boolean success, ExecutionTimer timer) throws Exception { + boolean logExecutionTime = getOptions().logExecutionTime(); + if (success && getOptions().printSucceedingStatements()) { + System.out.println(q.getLogString()); + } + if (logExecutionTime) { + getLogger().writeCurrent("// " + timer.end().asString()); + } + if (q.couldAffectSchema()) { + updateSchema(); + } + } + + @Override + protected MongoDBSchema readSchema() throws Exception { + return new MongoDBSchema(schemaTables); + } + } + + @Override + public void generateDatabase(MongoDBGlobalState globalState) throws Exception { + for (int i = 0; i < Randomly.fromOptions(4, 5, 6); i++) { + boolean success; + do { + MongoDBQueryAdapter query = new MongoDBTableGenerator().getQuery(globalState); + success = globalState.executeStatement(query); + } while (!success); + } + /* + * StatementExecutor se = new StatementExecutor<>(globalState, Action.values(), + * MongoDBProvider::mapActions, (q) -> { if(globalState.getSchema().getDatabaseTables().isEmpty()) { throw new + * IgnoreMeException(); } }); se.executeStatements(); + */ + } + + @Override + public MongoDBConnection createDatabase(MongoDBGlobalState globalState) throws Exception { + MongoClient mongoClient = MongoClients.create(); + MongoDatabase database = mongoClient.getDatabase(globalState.getDatabaseName()); + database.drop(); + return new MongoDBConnection(mongoClient, database); + } + + @Override + public String getDBMSName() { + return "mongodb"; + } + + @Override + public LoggableFactory getLoggableFactory() { + return new MongoDBLoggableFactory(); + } + + @Override + protected void checkViewsAreValid(MongoDBGlobalState globalState) { + } +} diff --git a/src/sqlancer/mongodb/MongoDBQueryAdapter.java b/src/sqlancer/mongodb/MongoDBQueryAdapter.java new file mode 100644 index 000000000..438163486 --- /dev/null +++ b/src/sqlancer/mongodb/MongoDBQueryAdapter.java @@ -0,0 +1,21 @@ +package sqlancer.mongodb; + +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.Query; + +public abstract class MongoDBQueryAdapter extends Query { + @Override + public String getQueryString() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUnterminatedQueryString() { + throw new UnsupportedOperationException(); + } + + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } +} diff --git a/src/sqlancer/mongodb/MongoDBQueryProvider.java b/src/sqlancer/mongodb/MongoDBQueryProvider.java new file mode 100644 index 000000000..970c90cea --- /dev/null +++ b/src/sqlancer/mongodb/MongoDBQueryProvider.java @@ -0,0 +1,6 @@ +package sqlancer.mongodb; + +@FunctionalInterface +public interface MongoDBQueryProvider { + MongoDBQueryAdapter getQuery(S globalState) throws Exception; +} diff --git a/src/sqlancer/mongodb/MongoDBSchema.java b/src/sqlancer/mongodb/MongoDBSchema.java new file mode 100644 index 000000000..5bf3cf2b5 --- /dev/null +++ b/src/sqlancer/mongodb/MongoDBSchema.java @@ -0,0 +1,117 @@ +package sqlancer.mongodb; + +import java.util.Collections; +import java.util.List; + +import org.bson.BsonType; + +import com.mongodb.client.MongoDatabase; + +import sqlancer.Randomly; +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; +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; + +public class MongoDBSchema extends AbstractSchema { + + public enum MongoDBDataType implements HasBsonType { + INTEGER { + @Override + public BsonType getBsonType() { + return BsonType.INT32; + } + }, + STRING { + @Override + public BsonType getBsonType() { + return BsonType.STRING; + } + }, + BOOLEAN { + @Override + public BsonType getBsonType() { + return BsonType.BOOLEAN; + } + }, + DOUBLE { + @Override + public BsonType getBsonType() { + return BsonType.DOUBLE; + } + }, + DATE_TIME { + @Override + public BsonType getBsonType() { + return BsonType.DATE_TIME; + } + }, + TIMESTAMP { + @Override + public BsonType getBsonType() { + return BsonType.TIMESTAMP; + } + }; + + public static MongoDBDataType getRandom() { + return Randomly.fromOptions(values()); + } + } + + public interface HasBsonType { + BsonType getBsonType(); + } + + public static class MongoDBColumn extends AbstractTableColumn { + + private final boolean isId; + private final boolean isNullable; + + public MongoDBColumn(String name, MongoDBDataType type, boolean isId, boolean isNullable) { + super(name, null, type); + this.isId = isId; + this.isNullable = isNullable; + } + + public boolean isId() { + return isId; + } + + public boolean isNullable() { + return isNullable; + } + + } + + public static class MongoDBTables extends AbstractTables { + + public MongoDBTables(List tables) { + super(tables); + } + } + + public MongoDBSchema(List databaseTables) { + super(databaseTables); + } + + public static class MongoDBTable extends AbstractTable { + public MongoDBTable(String name, List columns, boolean isView) { + super(name, columns, Collections.emptyList(), isView); + } + + @Override + public long getNrRows(MongoDBGlobalState globalState) { + throw new UnsupportedOperationException(); + } + } + + public static MongoDBSchema fromConnection(MongoDatabase connection, String databaseName) { + throw new UnsupportedOperationException(); + } + + public MongoDBTables getRandomTableNonEmptyTables() { + return new MongoDBTables(Randomly.nonEmptySubset(getDatabaseTables())); + } +} diff --git a/src/sqlancer/mongodb/ast/MongoDBExpression.java b/src/sqlancer/mongodb/ast/MongoDBExpression.java new file mode 100644 index 000000000..1235a1fbc --- /dev/null +++ b/src/sqlancer/mongodb/ast/MongoDBExpression.java @@ -0,0 +1,4 @@ +package sqlancer.mongodb.ast; + +public interface MongoDBExpression { +} diff --git a/src/sqlancer/mongodb/gen/MongoDBTableGenerator.java b/src/sqlancer/mongodb/gen/MongoDBTableGenerator.java new file mode 100644 index 000000000..f71ea6411 --- /dev/null +++ b/src/sqlancer/mongodb/gen/MongoDBTableGenerator.java @@ -0,0 +1,49 @@ +package sqlancer.mongodb.gen; + +import java.util.ArrayList; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.MongoDBSchema.MongoDBColumn; +import sqlancer.mongodb.MongoDBSchema.MongoDBDataType; +import sqlancer.mongodb.MongoDBSchema.MongoDBTable; +import sqlancer.mongodb.query.MongoDBCreateTableQuery; + +public class MongoDBTableGenerator { + + private MongoDBTable table; + private final List columnsToBeAdded = new ArrayList<>(); + + public MongoDBQueryAdapter getQuery(MongoDBGlobalState globalState) { + String tableName = globalState.getSchema().getFreeTableName(); + MongoDBCreateTableQuery createTableQuery = new MongoDBCreateTableQuery(tableName); + table = new MongoDBTable(tableName, columnsToBeAdded, false); + for (int i = 0; i < Randomly.smallNumber() + 1; i++) { + String columnName = String.format("c%d", i); + MongoDBDataType type = createColumn(columnName); + if (globalState.getDmbsSpecificOptions().testValidation) { + createTableQuery.addValidation(columnName, type.getBsonType()); + } + } + globalState.addTable(table); + return createTableQuery; + } + + private MongoDBDataType createColumn(String columnName) { + MongoDBDataType columnType = MongoDBDataType.getRandom(); + MongoDBColumn newColumn = new MongoDBColumn(columnName, columnType, false, false); + newColumn.setTable(table); + columnsToBeAdded.add(newColumn); + return columnType; + } + + public String getTableName() { + return table.getName(); + } + + public MongoDBTable getGeneratedTable() { + return table; + } +} diff --git a/src/sqlancer/mongodb/query/MongoDBCreateTableQuery.java b/src/sqlancer/mongodb/query/MongoDBCreateTableQuery.java new file mode 100644 index 000000000..da4381079 --- /dev/null +++ b/src/sqlancer/mongodb/query/MongoDBCreateTableQuery.java @@ -0,0 +1,109 @@ +package sqlancer.mongodb.query; + +import java.util.ArrayList; +import java.util.List; + +import org.bson.BsonType; +import org.bson.conversions.Bson; + +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.ValidationOptions; + +import sqlancer.GlobalState; +import sqlancer.Main; +import sqlancer.mongodb.MongoDBConnection; +import sqlancer.mongodb.MongoDBQueryAdapter; + +public class MongoDBCreateTableQuery extends MongoDBQueryAdapter { + + private final String tableName; + private Bson validationFilter; + private final List logRequiredList; + private final List logPropertiesList; + + public MongoDBCreateTableQuery(String tableName) { + this.tableName = tableName; + this.validationFilter = null; + logRequiredList = new ArrayList<>(); + logPropertiesList = new ArrayList<>(); + } + + @Override + public boolean couldAffectSchema() { + return true; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + ValidationOptions collOptions = new ValidationOptions().validator(this.validationFilter); + Main.nrSuccessfulActions.addAndGet(1); + globalState.getConnection().getDatabase().createCollection(tableName, + new CreateCollectionOptions().validationOptions(collOptions)); + return true; + } + + @Override + public String getLogString() { + String helper = ""; + StringBuilder sb = new StringBuilder(); + sb.append("db.createCollection(\"").append(tableName).append("\", {\n"); + + if (!logPropertiesList.isEmpty()) { + sb.append("validator: {"); + sb.append("$jsonSchema: {"); + sb.append("bsonType:\"object\","); + sb.append("required: [\n"); + for (String req : logRequiredList) { + sb.append(helper); + helper = ","; + sb.append(req); + } + sb.append("],"); + sb.append("properties: {\n"); + for (String prop : logPropertiesList) { + sb.append(prop); + } + sb.append("}}}})"); + } else { + sb.append("})"); + } + + return sb.toString(); + } + + public void addValidation(String columnName, BsonType type) { + Bson nameFilter = Filters.exists(columnName); + Bson typeFilter = Filters.type(columnName, type); + + if (validationFilter == null) { + validationFilter = Filters.and(nameFilter, typeFilter); + } else { + validationFilter = Filters.and(validationFilter, Filters.and(nameFilter, typeFilter)); + } + + logRequiredList.add("\"" + columnName + "\""); + logPropertiesList.add(columnName + ": { bsonType:\"" + bsonTypeToString(type) + "\"},\n"); + } + + public String bsonTypeToString(BsonType type) { + switch (type) { + case DOUBLE: + return "double"; + case STRING: + return "string"; + case BOOLEAN: + return "bool"; + case INT32: + case INT64: + return "int"; + case DATE_TIME: + return "date"; + case TIMESTAMP: + return "timestamp"; + default: + throw new IllegalStateException(); + } + } +} diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java new file mode 100644 index 000000000..1f59c1909 --- /dev/null +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java @@ -0,0 +1,26 @@ +package sqlancer.mongodb.test; + +import sqlancer.common.ast.newast.Node; +import sqlancer.common.gen.ExpressionGenerator; +import sqlancer.common.oracle.TernaryLogicPartitioningOracleBase; +import sqlancer.common.oracle.TestOracle; +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.ast.MongoDBExpression; + +public class MongoDBQueryPartitioningBase + extends TernaryLogicPartitioningOracleBase, MongoDBGlobalState> implements TestOracle { + + public MongoDBQueryPartitioningBase(MongoDBGlobalState state) { + super(state); + } + + @Override + public void check() throws Exception { + + } + + @Override + protected ExpressionGenerator> getGen() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java new file mode 100644 index 000000000..5ee795011 --- /dev/null +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java @@ -0,0 +1,9 @@ +package sqlancer.mongodb.test; + +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; + +public class MongoDBQueryPartitioningWhereTester extends MongoDBQueryPartitioningBase { + public MongoDBQueryPartitioningWhereTester(MongoDBGlobalState state) { + super(state); + } +} From 24e8897da646d1bf3ed5d197fb7835e5bdcf32bf Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 6 Jan 2021 16:13:06 +0100 Subject: [PATCH 02/24] Add data inserts and index creation for MongoDB Random Document insertion with the option to follow the schema validation or set random types is now supported. Also an option has been added to randomly set an insert to null. Validation has to be turned off for these options though. Additionally, random indexes are created over random columns, either ascending or descending and when there are multiple indexes added for a single collection it creates a composite index out of the other ones. --- src/sqlancer/mongodb/MongoDBOptions.java | 9 + src/sqlancer/mongodb/MongoDBProvider.java | 58 ++++-- src/sqlancer/mongodb/ast/MongoDBConstant.java | 175 ++++++++++++++++++ .../mongodb/gen/MongoDBConstantGenerator.java | 66 +++++++ .../mongodb/gen/MongoDBIndexGenerator.java | 25 +++ .../mongodb/gen/MongoDBInsertGenerator.java | 38 ++++ .../query/MongoDBCreateIndexQuery.java | 71 +++++++ .../mongodb/query/MongoDBInsertQuery.java | 79 ++++++++ 8 files changed, 501 insertions(+), 20 deletions(-) create mode 100644 src/sqlancer/mongodb/ast/MongoDBConstant.java create mode 100644 src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java create mode 100644 src/sqlancer/mongodb/gen/MongoDBIndexGenerator.java create mode 100644 src/sqlancer/mongodb/gen/MongoDBInsertGenerator.java create mode 100644 src/sqlancer/mongodb/query/MongoDBCreateIndexQuery.java create mode 100644 src/sqlancer/mongodb/query/MongoDBInsertQuery.java diff --git a/src/sqlancer/mongodb/MongoDBOptions.java b/src/sqlancer/mongodb/MongoDBOptions.java index 58086ac2f..a6306b515 100644 --- a/src/sqlancer/mongodb/MongoDBOptions.java +++ b/src/sqlancer/mongodb/MongoDBOptions.java @@ -19,6 +19,15 @@ public class MongoDBOptions implements DBMSSpecificOptions oracles = Arrays.asList(QUERY_PARTITIONING); diff --git a/src/sqlancer/mongodb/MongoDBProvider.java b/src/sqlancer/mongodb/MongoDBProvider.java index 5fd4801a0..cf4375d01 100644 --- a/src/sqlancer/mongodb/MongoDBProvider.java +++ b/src/sqlancer/mongodb/MongoDBProvider.java @@ -7,13 +7,18 @@ import com.mongodb.client.MongoClients; import com.mongodb.client.MongoDatabase; +import sqlancer.AbstractAction; import sqlancer.ExecutionTimer; import sqlancer.GlobalState; +import sqlancer.IgnoreMeException; import sqlancer.ProviderAdapter; import sqlancer.Randomly; +import sqlancer.StatementExecutor; import sqlancer.common.log.LoggableFactory; import sqlancer.common.query.Query; import sqlancer.mongodb.MongoDBSchema.MongoDBTable; +import sqlancer.mongodb.gen.MongoDBIndexGenerator; +import sqlancer.mongodb.gen.MongoDBInsertGenerator; import sqlancer.mongodb.gen.MongoDBTableGenerator; public class MongoDBProvider @@ -23,21 +28,32 @@ public MongoDBProvider() { super(MongoDBGlobalState.class, MongoDBOptions.class); } - /* - * enum Action implements AbstractAction { ; INSERT(MongoDBInsertGenerator::getQuery); - * CREATE_INDEX(MongoDBIndexGenerator::getQuery); - * - * private final MongoDBQueryProvider queryProvider; - * - * Action(MongoDBQueryProvider queryProvider) { this.queryProvider = queryProvider; } - * - * @Override public Query getQuery(MongoDBGlobalState globalState) throws Exception { return - * queryProvider.getQuery(globalState); } } - * - * private static int mapActions(MongoDBGlobalState globalState, Action a) { Randomly r = globalState.getRandomly(); - * switch (a) { case INSERT: return r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); case - * CREATE_INDEX: return r.getInteger(0, 2); default: throw new AssertionError(a); } return -1; } - */ + enum Action implements AbstractAction { + INSERT(MongoDBInsertGenerator::getQuery), CREATE_INDEX(MongoDBIndexGenerator::getQuery); + + private final MongoDBQueryProvider queryProvider; + + Action(MongoDBQueryProvider queryProvider) { + this.queryProvider = queryProvider; + } + + @Override + public Query getQuery(MongoDBGlobalState globalState) throws Exception { + return queryProvider.getQuery(globalState); + } + } + + private static int mapActions(MongoDBGlobalState globalState, Action a) { + Randomly r = globalState.getRandomly(); + switch (a) { + case INSERT: + return r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); + case CREATE_INDEX: + return r.getInteger(0, globalState.getDmbsSpecificOptions().maxNumberIndexes); + default: + throw new AssertionError(a); + } + } public static class MongoDBGlobalState extends GlobalState { @@ -76,11 +92,13 @@ public void generateDatabase(MongoDBGlobalState globalState) throws Exception { success = globalState.executeStatement(query); } while (!success); } - /* - * StatementExecutor se = new StatementExecutor<>(globalState, Action.values(), - * MongoDBProvider::mapActions, (q) -> { if(globalState.getSchema().getDatabaseTables().isEmpty()) { throw new - * IgnoreMeException(); } }); se.executeStatements(); - */ + StatementExecutor se = new StatementExecutor<>(globalState, Action.values(), + MongoDBProvider::mapActions, (q) -> { + if (globalState.getSchema().getDatabaseTables().isEmpty()) { + throw new IgnoreMeException(); + } + }); + se.executeStatements(); } @Override diff --git a/src/sqlancer/mongodb/ast/MongoDBConstant.java b/src/sqlancer/mongodb/ast/MongoDBConstant.java new file mode 100644 index 000000000..75caa0efd --- /dev/null +++ b/src/sqlancer/mongodb/ast/MongoDBConstant.java @@ -0,0 +1,175 @@ +package sqlancer.mongodb.ast; + +import org.bson.BsonDateTime; +import org.bson.BsonTimestamp; +import org.bson.Document; + +import sqlancer.common.ast.newast.Node; + +public abstract class MongoDBConstant implements Node { + private MongoDBConstant() { + } + + public abstract void setValueInDocument(Document document, String key); + + public abstract String getLogValue(); + + public static class MongoDBNullConstant extends MongoDBConstant { + + @Override + public void setValueInDocument(Document document, String key) { + document.append(key, null); + } + + @Override + public String getLogValue() { + return "null"; + } + } + + public static class MongoDBIntegerConstant extends MongoDBConstant { + + private final int value; + + public MongoDBIntegerConstant(int value) { + this.value = value; + } + + @Override + public void setValueInDocument(Document document, String key) { + document.append(key, value); + } + + @Override + public String getLogValue() { + return String.valueOf(value); + } + } + + public static Node createIntegerConstant(int value) { + return new MongoDBIntegerConstant(value); + } + + public static class MongoDBStringConstant extends MongoDBConstant { + + private final String value; + + public MongoDBStringConstant(String value) { + this.value = value; + } + + public String getStringValue() { + return value; + } + + @Override + public void setValueInDocument(Document document, String key) { + document.append(key, value); + } + + @Override + public String getLogValue() { + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + } + + public static Node createStringConstant(String value) { + return new MongoDBStringConstant(value); + } + + public static class MongoDBBooleanConstant extends MongoDBConstant { + + private final boolean value; + + public MongoDBBooleanConstant(boolean value) { + this.value = value; + } + + @Override + public void setValueInDocument(Document document, String key) { + document.append(key, value); + } + + @Override + public String getLogValue() { + if (value) { + return "true"; + } + return "false"; + } + } + + public static Node createBooleanConstant(boolean value) { + return new MongoDBBooleanConstant(value); + } + + public static class MongoDBDoubleConstant extends MongoDBConstant { + + private final double value; + + public MongoDBDoubleConstant(double value) { + this.value = value; + } + + @Override + public void setValueInDocument(Document document, String key) { + document.append(key, value); + } + + @Override + public String getLogValue() { + return String.valueOf(value); + } + } + + public static Node createDoubleConstant(double value) { + return new MongoDBDoubleConstant(value); + } + + public static class MongoDBDateTimeConstant extends MongoDBConstant { + + private final BsonDateTime value; + + public MongoDBDateTimeConstant(long val) { + this.value = new BsonDateTime(val); + } + + @Override + public void setValueInDocument(Document document, String key) { + document.append(key, value); + } + + @Override + public String getLogValue() { + return String.valueOf(value); + } + } + + public static Node createDateTimeConstant(long value) { + return new MongoDBDateTimeConstant(value); + } + + public static class MongoDBTimestampConstant extends MongoDBConstant { + + private final BsonTimestamp value; + + public MongoDBTimestampConstant(long value) { + this.value = new BsonTimestamp(value); + } + + @Override + public void setValueInDocument(Document document, String key) { + document.append(key, value); + } + + @Override + public String getLogValue() { + return String.valueOf(value); + } + } + + public static Node createTimestampConstant(long value) { + return new MongoDBTimestampConstant(value); + } + +} diff --git a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java new file mode 100644 index 000000000..090016809 --- /dev/null +++ b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java @@ -0,0 +1,66 @@ +package sqlancer.mongodb.gen; + +import org.bson.Document; + +import sqlancer.Randomly; +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.MongoDBSchema.MongoDBDataType; +import sqlancer.mongodb.ast.MongoDBConstant; +import sqlancer.mongodb.ast.MongoDBConstant.MongoDBBooleanConstant; +import sqlancer.mongodb.ast.MongoDBConstant.MongoDBDateTimeConstant; +import sqlancer.mongodb.ast.MongoDBConstant.MongoDBDoubleConstant; +import sqlancer.mongodb.ast.MongoDBConstant.MongoDBIntegerConstant; +import sqlancer.mongodb.ast.MongoDBConstant.MongoDBNullConstant; +import sqlancer.mongodb.ast.MongoDBConstant.MongoDBStringConstant; +import sqlancer.mongodb.ast.MongoDBConstant.MongoDBTimestampConstant; + +public class MongoDBConstantGenerator { + private final MongoDBGlobalState globalState; + + public MongoDBConstantGenerator(MongoDBGlobalState globalState) { + this.globalState = globalState; + } + + public void addRandomConstant(Document document, String key) { + MongoDBDataType type = MongoDBDataType.getRandom(); + addRandomConstantWithType(document, key, type); + } + + public void addRandomConstantWithType(Document document, String key, MongoDBDataType option) { + MongoDBConstant constant; + if (globalState.getDmbsSpecificOptions().testNullInserts && Randomly.getBooleanWithSmallProbability()) { + constant = new MongoDBNullConstant(); + constant.setValueInDocument(document, key); + return; + } + switch (option) { + case DATE_TIME: + constant = new MongoDBDateTimeConstant(globalState.getRandomly().getInteger()); + constant.setValueInDocument(document, key); + return; + + case BOOLEAN: + constant = new MongoDBBooleanConstant(Randomly.getBoolean()); + constant.setValueInDocument(document, key); + return; + case DOUBLE: + constant = new MongoDBDoubleConstant(globalState.getRandomly().getDouble()); + constant.setValueInDocument(document, key); + return; + case STRING: + constant = new MongoDBStringConstant(globalState.getRandomly().getString()); + constant.setValueInDocument(document, key); + return; + case INTEGER: + constant = new MongoDBIntegerConstant((int) globalState.getRandomly().getInteger()); + constant.setValueInDocument(document, key); + return; + case TIMESTAMP: + constant = new MongoDBTimestampConstant(globalState.getRandomly().getInteger()); + constant.setValueInDocument(document, key); + return; + default: + throw new AssertionError(option); + } + } +} diff --git a/src/sqlancer/mongodb/gen/MongoDBIndexGenerator.java b/src/sqlancer/mongodb/gen/MongoDBIndexGenerator.java new file mode 100644 index 000000000..8687fd45c --- /dev/null +++ b/src/sqlancer/mongodb/gen/MongoDBIndexGenerator.java @@ -0,0 +1,25 @@ +package sqlancer.mongodb.gen; + +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.MongoDBSchema.MongoDBColumn; +import sqlancer.mongodb.MongoDBSchema.MongoDBTable; +import sqlancer.mongodb.query.MongoDBCreateIndexQuery; + +public final class MongoDBIndexGenerator { + private MongoDBIndexGenerator() { + } + + public static MongoDBQueryAdapter getQuery(MongoDBGlobalState globalState) { + MongoDBTable randomTable = globalState.getSchema().getRandomTable(); + List columns = Randomly.nonEmptySubset(randomTable.getColumns()); + MongoDBCreateIndexQuery createIndexQuery = new MongoDBCreateIndexQuery(randomTable); + for (MongoDBColumn column : columns) { + createIndexQuery.addIndex(column.getName(), Randomly.getBoolean()); + } + return createIndexQuery; + } +} diff --git a/src/sqlancer/mongodb/gen/MongoDBInsertGenerator.java b/src/sqlancer/mongodb/gen/MongoDBInsertGenerator.java new file mode 100644 index 000000000..4501971b4 --- /dev/null +++ b/src/sqlancer/mongodb/gen/MongoDBInsertGenerator.java @@ -0,0 +1,38 @@ +package sqlancer.mongodb.gen; + +import org.bson.Document; + +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.MongoDBSchema.MongoDBTable; +import sqlancer.mongodb.query.MongoDBInsertQuery; + +public final class MongoDBInsertGenerator { + + private final MongoDBGlobalState globalState; + + private MongoDBInsertGenerator(MongoDBGlobalState globalState) { + this.globalState = globalState; + } + + public static MongoDBQueryAdapter getQuery(MongoDBGlobalState globalState) { + return new MongoDBInsertGenerator(globalState).generate(); + } + + public MongoDBQueryAdapter generate() { + Document result = new Document(); + MongoDBTable table = globalState.getSchema().getRandomTable(); + MongoDBConstantGenerator constantGenerator = new MongoDBConstantGenerator(globalState); + + for (int i = 0; i < table.getColumns().size(); i++) { + if (!globalState.getDmbsSpecificOptions().testRandomTypes) { + constantGenerator.addRandomConstantWithType(result, table.getColumns().get(i).getName(), + table.getColumns().get(i).getType()); + } else { + constantGenerator.addRandomConstant(result, table.getColumns().get(i).getName()); + } + } + + return new MongoDBInsertQuery(table, result); + } +} diff --git a/src/sqlancer/mongodb/query/MongoDBCreateIndexQuery.java b/src/sqlancer/mongodb/query/MongoDBCreateIndexQuery.java new file mode 100644 index 000000000..8d54d9cf7 --- /dev/null +++ b/src/sqlancer/mongodb/query/MongoDBCreateIndexQuery.java @@ -0,0 +1,71 @@ +package sqlancer.mongodb.query; + +import java.util.ArrayList; +import java.util.List; + +import org.bson.conversions.Bson; + +import com.mongodb.client.model.Indexes; + +import sqlancer.GlobalState; +import sqlancer.Main; +import sqlancer.mongodb.MongoDBConnection; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.MongoDBSchema.MongoDBTable; + +public class MongoDBCreateIndexQuery extends MongoDBQueryAdapter { + + private final MongoDBTable table; + private final List indeces; + private final List logIndeces; + + public MongoDBCreateIndexQuery(MongoDBTable table) { + this.table = table; + this.indeces = new ArrayList<>(); + this.logIndeces = new ArrayList<>(); + } + + public void addIndex(String column, boolean ascending) { + if (ascending) { + indeces.add(Indexes.ascending(column)); + logIndeces.add(column + ": 1"); + } else { + indeces.add(Indexes.descending(column)); + logIndeces.add(column + ": -1"); + } + } + + @Override + public String getLogString() { + StringBuilder sb = new StringBuilder(); + sb.append("db.").append(table.getName()).append(".createIndex({"); + String helper = ""; + for (String index : logIndeces) { + sb.append(helper); + helper = ","; + sb.append(index); + } + sb.append("})\n"); + return sb.toString(); + } + + @Override + public boolean couldAffectSchema() { + return false; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + Main.nrSuccessfulActions.addAndGet(1); + Bson index; + if (indeces.size() > 1) { + index = Indexes.compoundIndex(indeces); + } else { + index = indeces.get(0); + } + globalState.getConnection().getDatabase().getCollection(table.getName()).createIndex(index); + return true; + } + +} diff --git a/src/sqlancer/mongodb/query/MongoDBInsertQuery.java b/src/sqlancer/mongodb/query/MongoDBInsertQuery.java new file mode 100644 index 000000000..8276a0895 --- /dev/null +++ b/src/sqlancer/mongodb/query/MongoDBInsertQuery.java @@ -0,0 +1,79 @@ +package sqlancer.mongodb.query; + +import org.bson.BsonDateTime; +import org.bson.BsonTimestamp; +import org.bson.Document; +import org.bson.types.ObjectId; + +import com.mongodb.client.result.InsertOneResult; + +import sqlancer.GlobalState; +import sqlancer.mongodb.MongoDBConnection; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.MongoDBSchema.MongoDBTable; + +public class MongoDBInsertQuery extends MongoDBQueryAdapter { + boolean excluded; + private final MongoDBTable table; + private final Document documentToBeInserted; + + public MongoDBInsertQuery(MongoDBTable table, Document documentToBeInserted) { + this.table = table; + this.documentToBeInserted = documentToBeInserted; + this.excluded = false; + } + + @Override + public String getLogString() { + StringBuilder sb = new StringBuilder(); + sb.append("db." + table.getName() + ".insert({"); + String helper = ""; + for (String key : documentToBeInserted.keySet()) { + sb.append(helper); + helper = ", "; + if (documentToBeInserted.get(key) instanceof ObjectId) { + continue; + } + Object value = documentToBeInserted.get(key); + sb.append(key); + sb.append(": "); + sb.append(getStringRepresentation(value)); + } + sb.append("})\n"); + + return sb.toString(); + } + + private String getStringRepresentation(Object value) { + if (value instanceof Double) { + return String.valueOf(value); + } else if (value instanceof Integer) { + return "NumberInt(" + value + ")"; + } else if (value instanceof String) { + return "\"" + value + "\""; + } else if (value instanceof BsonDateTime) { + return "new Date(" + ((BsonDateTime) value).getValue() + ")"; + } else if (value instanceof BsonTimestamp) { + return "Timestamp(" + ((BsonTimestamp) value).getValue() + ",1)"; + } else if (value instanceof Boolean) { + return String.valueOf(value); + } else if (value == null) { + return "null"; + } else { + throw new IllegalStateException(); + } + } + + @Override + public boolean couldAffectSchema() { + return true; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + InsertOneResult result = globalState.getConnection().getDatabase().getCollection(table.getName()) + .insertOne(documentToBeInserted); + return result.wasAcknowledged(); + } +} From 46d168085531a9330323f64d7eff7ead261dec08 Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 6 Jan 2021 22:30:07 +0100 Subject: [PATCH 03/24] Add query execution for MongoDB with lookup and project stages The project stage is similar to the SELECT in SQL where we can specify which columns are returned. The Lookup stage is similar to the LEFT OUTER JOIN of SQL, for this we have to specifically make new random columns that specify the join column. --- .../mongodb/ast/MongoDBDummyPredicate.java | 7 ++ src/sqlancer/mongodb/ast/MongoDBSelect.java | 76 +++++++++++++++++++ .../gen/MongoDBExpressionGenerator.java | 40 ++++++++++ .../mongodb/query/MongoDBSelectQuery.java | 75 ++++++++++++++++++ .../test/MongoDBColumnTestReference.java | 34 +++++++++ .../test/MongoDBQueryPartitioningBase.java | 51 ++++++++++++- .../MongoDBQueryPartitioningWhereTester.java | 22 ++++++ .../mongodb/visitor/MongoDBToLogVisitor.java | 72 ++++++++++++++++++ .../visitor/MongoDBToQueryVisitor.java | 63 +++++++++++++++ .../mongodb/visitor/MongoDBVisitor.java | 38 ++++++++++ 10 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 src/sqlancer/mongodb/ast/MongoDBDummyPredicate.java create mode 100644 src/sqlancer/mongodb/ast/MongoDBSelect.java create mode 100644 src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java create mode 100644 src/sqlancer/mongodb/query/MongoDBSelectQuery.java create mode 100644 src/sqlancer/mongodb/test/MongoDBColumnTestReference.java create mode 100644 src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java create mode 100644 src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java create mode 100644 src/sqlancer/mongodb/visitor/MongoDBVisitor.java diff --git a/src/sqlancer/mongodb/ast/MongoDBDummyPredicate.java b/src/sqlancer/mongodb/ast/MongoDBDummyPredicate.java new file mode 100644 index 000000000..a9ecc3325 --- /dev/null +++ b/src/sqlancer/mongodb/ast/MongoDBDummyPredicate.java @@ -0,0 +1,7 @@ +package sqlancer.mongodb.ast; + +import sqlancer.common.ast.newast.Node; + +public class MongoDBDummyPredicate implements Node { + +} diff --git a/src/sqlancer/mongodb/ast/MongoDBSelect.java b/src/sqlancer/mongodb/ast/MongoDBSelect.java new file mode 100644 index 000000000..c6252893f --- /dev/null +++ b/src/sqlancer/mongodb/ast/MongoDBSelect.java @@ -0,0 +1,76 @@ +package sqlancer.mongodb.ast; + +import java.util.List; + +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.test.MongoDBColumnTestReference; + +public class MongoDBSelect implements Node { + + private final String mainTableName; + private final MongoDBColumnTestReference joinColumn; + List projectionColumns; + List lookupList; + boolean hasFilter; + Node filterClause; + + public MongoDBSelect(String mainTableName, MongoDBColumnTestReference joinColumn) { + this.mainTableName = mainTableName; + this.joinColumn = joinColumn; + } + + public String getMainTableName() { + return mainTableName; + } + + public MongoDBColumnTestReference getJoinColumn() { + return joinColumn; + } + + public void setProjectionList(List fetchColumns) { + if (fetchColumns == null || fetchColumns.isEmpty()) { + throw new IllegalArgumentException(); + } + this.projectionColumns = fetchColumns; + } + + public List getProjectionList() { + if (projectionColumns == null) { + throw new IllegalStateException(); + } + return projectionColumns; + } + + public void setLookupList(List lookupList) { + if (lookupList == null || lookupList.isEmpty()) { + throw new IllegalArgumentException(); + } + this.lookupList = lookupList; + } + + public List getLookupList() { + if (lookupList == null) { + throw new IllegalStateException(); + } + return lookupList; + } + + public void setFilterClause(Node filterClause) { + if (filterClause == null) { + hasFilter = false; + this.filterClause = null; + return; + } + hasFilter = true; + this.filterClause = filterClause; + } + + public Node getFilterClause() { + return filterClause; + } + + public boolean hasWhere() { + return hasFilter; + } + +} diff --git a/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java new file mode 100644 index 000000000..f2c05dda2 --- /dev/null +++ b/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java @@ -0,0 +1,40 @@ +package sqlancer.mongodb.gen; + +import sqlancer.common.ast.newast.Node; +import sqlancer.common.gen.UntypedExpressionGenerator; +import sqlancer.mongodb.ast.MongoDBDummyPredicate; +import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.test.MongoDBColumnTestReference; + +public class MongoDBExpressionGenerator + extends UntypedExpressionGenerator, MongoDBColumnTestReference> { + /* + * private final MongoDBGlobalState globalState; + * + * public MongoDBExpressionGenerator(MongoDBGlobalState globalState) { this.globalState = globalState; } + */ + @Override + public Node generateConstant() { + throw new UnsupportedOperationException(); + } + + @Override + protected Node generateExpression(int depth) { + return new MongoDBDummyPredicate<>(); + } + + @Override + protected Node generateColumn() { + throw new UnsupportedOperationException(); + } + + @Override + public Node negatePredicate(Node predicate) { + return new MongoDBDummyPredicate<>(); + } + + @Override + public Node isNull(Node expr) { + return new MongoDBDummyPredicate<>(); + } +} diff --git a/src/sqlancer/mongodb/query/MongoDBSelectQuery.java b/src/sqlancer/mongodb/query/MongoDBSelectQuery.java new file mode 100644 index 000000000..91480ed4b --- /dev/null +++ b/src/sqlancer/mongodb/query/MongoDBSelectQuery.java @@ -0,0 +1,75 @@ +package sqlancer.mongodb.query; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.bson.Document; +import org.bson.conversions.Bson; + +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; + +import sqlancer.GlobalState; +import sqlancer.common.query.SQLancerResultSet; +import sqlancer.mongodb.MongoDBConnection; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBSelect; +import sqlancer.mongodb.visitor.MongoDBVisitor; + +public class MongoDBSelectQuery extends MongoDBQueryAdapter { + + private final MongoDBSelect select; + + private List resultSet; + + public MongoDBSelectQuery(MongoDBSelect select) { + this.select = select; + } + + @Override + public boolean couldAffectSchema() { + return false; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + public > SQLancerResultSet executeAndGet(G globalState, + String... fills) throws Exception { + if (globalState.getOptions().logEachSelect()) { + globalState.getLogger().writeCurrent(this.getLogString()); + try { + globalState.getLogger().getCurrentFileWriter().flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + List pipeline = MongoDBVisitor.asQuery(select); + + MongoCollection collection = globalState.getConnection().getDatabase() + .getCollection(select.getMainTableName()); + MongoCursor cursor = collection.aggregate(pipeline).cursor(); + resultSet = new ArrayList<>(); + while (cursor.hasNext()) { + Document document = cursor.next(); + resultSet.add(document); + } + return null; + } + + @Override + public String getLogString() { + return MongoDBVisitor.asStringLog(select); + } + + public List getResultSet() { + return resultSet; + } + +} diff --git a/src/sqlancer/mongodb/test/MongoDBColumnTestReference.java b/src/sqlancer/mongodb/test/MongoDBColumnTestReference.java new file mode 100644 index 000000000..f6c8e9d94 --- /dev/null +++ b/src/sqlancer/mongodb/test/MongoDBColumnTestReference.java @@ -0,0 +1,34 @@ +package sqlancer.mongodb.test; + +import sqlancer.mongodb.MongoDBSchema.MongoDBColumn; + +public class MongoDBColumnTestReference { + + private final MongoDBColumn columnReference; + private final boolean inMainTable; + + public MongoDBColumnTestReference(MongoDBColumn columnReference, boolean inMainTable) { + this.columnReference = columnReference; + this.inMainTable = inMainTable; + } + + public String getQueryString() { + if (inMainTable) { + return this.columnReference.getName(); + } else { + return "join_" + this.columnReference.getTable().getName() + "." + this.columnReference.getName(); + } + } + + public boolean inMainTable() { + return inMainTable; + } + + public String getTableName() { + return this.columnReference.getTable().getName(); + } + + public String getPlainName() { + return this.columnReference.getName(); + } +} diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java index 1f59c1909..4fda2a64b 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java @@ -1,26 +1,75 @@ package sqlancer.mongodb.test; +import java.util.ArrayList; +import java.util.List; + +import sqlancer.Randomly; import sqlancer.common.ast.newast.Node; import sqlancer.common.gen.ExpressionGenerator; import sqlancer.common.oracle.TernaryLogicPartitioningOracleBase; import sqlancer.common.oracle.TestOracle; import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.MongoDBSchema; +import sqlancer.mongodb.MongoDBSchema.MongoDBColumn; +import sqlancer.mongodb.MongoDBSchema.MongoDBTable; +import sqlancer.mongodb.MongoDBSchema.MongoDBTables; import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBSelect; +import sqlancer.mongodb.gen.MongoDBExpressionGenerator; public class MongoDBQueryPartitioningBase extends TernaryLogicPartitioningOracleBase, MongoDBGlobalState> implements TestOracle { + protected MongoDBSchema schema; + protected MongoDBTables targetTables; + protected MongoDBTable mainTable; + protected List targetColumns; + protected MongoDBExpressionGenerator expressionGenerator; + protected MongoDBSelect select; + public MongoDBQueryPartitioningBase(MongoDBGlobalState state) { super(state); } @Override public void check() throws Exception { + schema = state.getSchema(); + targetTables = schema.getRandomTableNonEmptyTables(); + mainTable = targetTables.getTables().get(0); + generateTargetColumns(); + expressionGenerator = new MongoDBExpressionGenerator().setColumns(targetColumns); + initializeTernaryPredicateVariants(); + select = new MongoDBSelect<>(mainTable.getName(), targetColumns.get(0)); + select.setProjectionList(targetColumns); + if (Randomly.getBooleanWithRatherLowProbability()) { + select.setLookupList(targetColumns); + } else { + select.setLookupList(Randomly.nonEmptySubset(targetColumns)); + } + } + private void generateTargetColumns() { + targetColumns = new ArrayList<>(); + for (MongoDBColumn c : mainTable.getColumns()) { + targetColumns.add(new MongoDBColumnTestReference(c, true)); + } + List joinsOtherTables = new ArrayList<>(); + for (int i = 1; i < targetTables.getTables().size(); i++) { + MongoDBTable procTable = targetTables.getTables().get(i); + for (MongoDBColumn c : procTable.getColumns()) { + joinsOtherTables.add(new MongoDBColumnTestReference(c, false)); + } + } + if (!joinsOtherTables.isEmpty()) { + int randNumber = state.getRandomly().getInteger(1, Math.min(joinsOtherTables.size(), 4)); + List subsetJoinsOtherTables = Randomly.nonEmptySubset(joinsOtherTables, + randNumber); + targetColumns.addAll(subsetJoinsOtherTables); + } } @Override protected ExpressionGenerator> getGen() { - throw new UnsupportedOperationException(); + return expressionGenerator; } } diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java index 5ee795011..11ae2c8ae 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java @@ -1,9 +1,31 @@ package sqlancer.mongodb.test; import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.query.MongoDBSelectQuery; public class MongoDBQueryPartitioningWhereTester extends MongoDBQueryPartitioningBase { public MongoDBQueryPartitioningWhereTester(MongoDBGlobalState state) { super(state); } + + @Override + public void check() throws Exception { + super.check(); + + select.setFilterClause(null); + MongoDBSelectQuery q = new MongoDBSelectQuery(select); + q.executeAndGet(state); + /* + * List firstResultSet = q.getResultSet(); select.setWhereClause(predicate); q = new + * MongoDBFindQueryAdapter(select, mainTable); q.executeAndGet(state); List secondResultSet = + * q.resultSet; + * + * select.setWhereClause(negatedPredicate); q = new MongoDBFindQueryAdapter(select, mainTable); + * q.executeAndGet(state); List thirdResultSet = q.resultSet; + * + * secondResultSet.addAll(thirdResultSet); MongoDBComparatorHelper.assumeResultSetsAreEqual(firstResultSet, + * secondResultSet, q); + * + */ + } } diff --git a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java new file mode 100644 index 000000000..50d43a3ee --- /dev/null +++ b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java @@ -0,0 +1,72 @@ +package sqlancer.mongodb.visitor; + +import java.util.ArrayList; +import java.util.List; + +import sqlancer.mongodb.ast.MongoDBConstant; +import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBSelect; +import sqlancer.mongodb.test.MongoDBColumnTestReference; + +public class MongoDBToLogVisitor extends MongoDBVisitor { + + private String mainTableName; + private List lookups; + private String projects; + + @Override + public void visit(MongoDBConstant c) { + throw new UnsupportedOperationException(); + } + + @Override + public void visit(MongoDBSelect select) { + mainTableName = select.getMainTableName(); + setLookups(select); + setProjects(select); + } + + private void setLookups(MongoDBSelect select) { + lookups = new ArrayList<>(); + for (MongoDBColumnTestReference testReference : select.getLookupList()) { + if (testReference.inMainTable()) { + continue; + } + String newLookup = "{ $lookup: { from: \"" + testReference.getTableName() + "\", localField: \"" + + select.getJoinColumn().getPlainName() + "\", foreignField: \"" + testReference.getPlainName() + + "\", as: \"" + testReference.getQueryString() + "\"}},\n"; + lookups.add(newLookup); + } + } + + private void setProjects(MongoDBSelect select) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + String helper = ""; + for (MongoDBColumnTestReference reference : select.getProjectionList()) { + sb.append(helper); + helper = ","; + sb.append("\"").append(reference.getQueryString()).append("\"").append(": 1"); + } + sb.append("}"); + projects = sb.toString(); + } + + public String getStringLog() { + StringBuilder sb = new StringBuilder(); + sb.append("db.").append(mainTableName).append(".aggregate([\n"); + for (String lookup : lookups) { + sb.append(lookup); + } + // sb.append(",\n"); + // if(select.hasWhere()) { + // sb.append("{ $match: "); + // sb.append(mongoDBToQueryVisitor.getFilterLog()); + // sb.append("},\n"); + // } + sb.append("{ $project : "); + sb.append(projects); + sb.append("}])\n"); + return sb.toString(); + } +} diff --git a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java new file mode 100644 index 000000000..3ad57a3cb --- /dev/null +++ b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java @@ -0,0 +1,63 @@ +package sqlancer.mongodb.visitor; + +import static com.mongodb.client.model.Aggregates.project; +import static com.mongodb.client.model.Projections.fields; +import static com.mongodb.client.model.Projections.include; + +import java.util.ArrayList; +import java.util.List; + +import org.bson.conversions.Bson; + +import com.mongodb.client.model.Aggregates; + +import sqlancer.mongodb.ast.MongoDBConstant; +import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBSelect; +import sqlancer.mongodb.test.MongoDBColumnTestReference; + +public class MongoDBToQueryVisitor extends MongoDBVisitor { + + private List lookup; + private Bson projection; + + @Override + public void visit(MongoDBConstant c) { + throw new UnsupportedOperationException(); + } + + @Override + public void visit(MongoDBSelect select) { + setLookup(select); + // if(select.hasWhere()) { + // // DO STUFF + // } + setProjection(select); + } + + private void setLookup(MongoDBSelect select) { + lookup = new ArrayList<>(); + for (MongoDBColumnTestReference reference : select.getLookupList()) { + if (reference.inMainTable()) { + continue; + } + lookup.add(Aggregates.lookup(reference.getTableName(), select.getJoinColumn().getPlainName(), + reference.getPlainName(), reference.getQueryString())); + } + } + + private void setProjection(MongoDBSelect select) { + List stringProjects = new ArrayList<>(); + for (MongoDBColumnTestReference ref : select.getProjectionList()) { + stringProjects.add(ref.getQueryString()); + } + projection = project(fields(include(stringProjects))); + } + + public List getPipeline() { + List result = new ArrayList<>(lookup); + // TODO Patrick: Add Match if where... + result.add(projection); + return result; + } +} diff --git a/src/sqlancer/mongodb/visitor/MongoDBVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBVisitor.java new file mode 100644 index 000000000..8c207a564 --- /dev/null +++ b/src/sqlancer/mongodb/visitor/MongoDBVisitor.java @@ -0,0 +1,38 @@ +package sqlancer.mongodb.visitor; + +import java.util.List; + +import org.bson.conversions.Bson; + +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.ast.MongoDBConstant; +import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBSelect; + +public abstract class MongoDBVisitor { + public abstract void visit(MongoDBConstant c); + + public abstract void visit(MongoDBSelect s); + + public void visit(Node expr) { + if (expr instanceof MongoDBConstant) { + visit((MongoDBConstant) expr); + } else if (expr instanceof MongoDBSelect) { + visit((MongoDBSelect) expr); + } else { + throw new AssertionError(expr.getClass()); + } + } + + public static List asQuery(Node expr) { + MongoDBToQueryVisitor visitor = new MongoDBToQueryVisitor(); + visitor.visit(expr); + return visitor.getPipeline(); + } + + public static String asStringLog(Node expr) { + MongoDBToLogVisitor visitor = new MongoDBToLogVisitor(); + visitor.visit(expr); + return visitor.getStringLog(); + } +} From 53a25cff0086be71ff7e439a7274628330fcabb2 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 7 Jan 2021 11:21:00 +0100 Subject: [PATCH 04/24] Implement expression generation and filter query execution for MongoDB The query ast has multiple key difference to the SQL version. The core is the MongoDBSelect class that holds projection and lookup lists. The filter is then a tree similar to the SQL version. In order to execute and log, which are different because one are API calls and the other MongoDB shell commands there exist two visitors, the ToQueryVisitor and the ToLogVisitor. --- .../mongodb/MongoDBComparatorHelper.java | 45 ++++ .../ast/MongoDBBinaryComparisonNode.java | 16 ++ .../mongodb/ast/MongoDBBinaryLogicalNode.java | 16 ++ src/sqlancer/mongodb/ast/MongoDBConstant.java | 54 ++++- src/sqlancer/mongodb/ast/MongoDBSelect.java | 2 +- .../ast/MongoDBUnaryLogicalOperatorNode.java | 16 ++ ....java => MongoDBUnsupportedPredicate.java} | 2 +- .../mongodb/gen/MongoDBConstantGenerator.java | 24 ++ .../gen/MongoDBExpressionGenerator.java | 211 +++++++++++++++++- .../test/MongoDBColumnTestReference.java | 8 +- .../test/MongoDBQueryPartitioningBase.java | 2 +- .../MongoDBQueryPartitioningWhereTester.java | 32 ++- .../mongodb/visitor/MongoDBToLogVisitor.java | 59 ++++- .../visitor/MongoDBToQueryVisitor.java | 55 ++++- .../mongodb/visitor/MongoDBVisitor.java | 1 + 15 files changed, 497 insertions(+), 46 deletions(-) create mode 100644 src/sqlancer/mongodb/MongoDBComparatorHelper.java create mode 100644 src/sqlancer/mongodb/ast/MongoDBBinaryComparisonNode.java create mode 100644 src/sqlancer/mongodb/ast/MongoDBBinaryLogicalNode.java create mode 100644 src/sqlancer/mongodb/ast/MongoDBUnaryLogicalOperatorNode.java rename src/sqlancer/mongodb/ast/{MongoDBDummyPredicate.java => MongoDBUnsupportedPredicate.java} (53%) diff --git a/src/sqlancer/mongodb/MongoDBComparatorHelper.java b/src/sqlancer/mongodb/MongoDBComparatorHelper.java new file mode 100644 index 000000000..235e5ece9 --- /dev/null +++ b/src/sqlancer/mongodb/MongoDBComparatorHelper.java @@ -0,0 +1,45 @@ +package sqlancer.mongodb; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bson.Document; + +import sqlancer.mongodb.query.MongoDBSelectQuery; + +public final class MongoDBComparatorHelper { + + private MongoDBComparatorHelper() { + } + + public static void assumeResultSetsAreEqual(List resultSet, List secondResultSet, + MongoDBSelectQuery originalQuery) { + if (resultSet.size() != secondResultSet.size()) { + String assertionMessage = String.format("The Size of the result sets mismatch (%d and %d)!\n%s", + resultSet.size(), resultSet.size(), originalQuery.getLogString()); + 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); + StringBuilder firstMisses = new StringBuilder(); + for (Document document : firstResultSetMisses) { + firstMisses.append(document.toJson()).append(" "); + } + StringBuilder secondMisses = new StringBuilder(); + for (Document document : secondResultSetMisses) { + secondMisses.append(document.toJson()).append(" "); + } + String assertMessage = String.format("The Content of the result sets mismatch!\n %s \n %s\n %s", + firstMisses.toString(), secondMisses.toString(), originalQuery.getLogString()); + throw new AssertionError(assertMessage); + } + } +} diff --git a/src/sqlancer/mongodb/ast/MongoDBBinaryComparisonNode.java b/src/sqlancer/mongodb/ast/MongoDBBinaryComparisonNode.java new file mode 100644 index 000000000..f5e3ea173 --- /dev/null +++ b/src/sqlancer/mongodb/ast/MongoDBBinaryComparisonNode.java @@ -0,0 +1,16 @@ +package sqlancer.mongodb.ast; + +import sqlancer.common.ast.newast.NewBinaryOperatorNode; +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.gen.MongoDBExpressionGenerator.MongoDBBinaryComparisonOperator; + +public class MongoDBBinaryComparisonNode extends NewBinaryOperatorNode { + public MongoDBBinaryComparisonNode(Node left, Node right, + MongoDBBinaryComparisonOperator op) { + super(left, right, op); + } + + public MongoDBBinaryComparisonOperator operator() { + return (MongoDBBinaryComparisonOperator) op; + } +} diff --git a/src/sqlancer/mongodb/ast/MongoDBBinaryLogicalNode.java b/src/sqlancer/mongodb/ast/MongoDBBinaryLogicalNode.java new file mode 100644 index 000000000..c3f18a248 --- /dev/null +++ b/src/sqlancer/mongodb/ast/MongoDBBinaryLogicalNode.java @@ -0,0 +1,16 @@ +package sqlancer.mongodb.ast; + +import sqlancer.common.ast.newast.NewBinaryOperatorNode; +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.gen.MongoDBExpressionGenerator.MongoDBBinaryLogicalOperator; + +public class MongoDBBinaryLogicalNode extends NewBinaryOperatorNode { + public MongoDBBinaryLogicalNode(Node left, Node right, + MongoDBBinaryLogicalOperator op) { + super(left, right, op); + } + + public MongoDBBinaryLogicalOperator operator() { + return (MongoDBBinaryLogicalOperator) op; + } +} diff --git a/src/sqlancer/mongodb/ast/MongoDBConstant.java b/src/sqlancer/mongodb/ast/MongoDBConstant.java index 75caa0efd..f52c18666 100644 --- a/src/sqlancer/mongodb/ast/MongoDBConstant.java +++ b/src/sqlancer/mongodb/ast/MongoDBConstant.java @@ -14,6 +14,8 @@ private MongoDBConstant() { public abstract String getLogValue(); + public abstract Object getValue(); + public static class MongoDBNullConstant extends MongoDBConstant { @Override @@ -25,6 +27,15 @@ public void setValueInDocument(Document document, String key) { public String getLogValue() { return "null"; } + + @Override + public Object getValue() { + return null; + } + } + + public static Node createNullConstant() { + return new MongoDBNullConstant(); } public static class MongoDBIntegerConstant extends MongoDBConstant { @@ -42,7 +53,12 @@ public void setValueInDocument(Document document, String key) { @Override public String getLogValue() { - return String.valueOf(value); + return "NumberInt(" + value + ")"; + } + + @Override + public Integer getValue() { + return value; } } @@ -69,7 +85,12 @@ public void setValueInDocument(Document document, String key) { @Override public String getLogValue() { - return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\""; + } + + @Override + public String getValue() { + return value; } } @@ -92,10 +113,12 @@ public void setValueInDocument(Document document, String key) { @Override public String getLogValue() { - if (value) { - return "true"; - } - return "false"; + return String.valueOf(value); + } + + @Override + public Boolean getValue() { + return value; } } @@ -120,6 +143,11 @@ public void setValueInDocument(Document document, String key) { public String getLogValue() { return String.valueOf(value); } + + @Override + public Double getValue() { + return value; + } } public static Node createDoubleConstant(double value) { @@ -141,7 +169,12 @@ public void setValueInDocument(Document document, String key) { @Override public String getLogValue() { - return String.valueOf(value); + return "new Date(" + value.getValue() + ")"; + } + + @Override + public BsonDateTime getValue() { + return value; } } @@ -164,7 +197,12 @@ public void setValueInDocument(Document document, String key) { @Override public String getLogValue() { - return String.valueOf(value); + return "Timestamp(" + value.getValue() + ",1)"; + } + + @Override + public BsonTimestamp getValue() { + return value; } } diff --git a/src/sqlancer/mongodb/ast/MongoDBSelect.java b/src/sqlancer/mongodb/ast/MongoDBSelect.java index c6252893f..39b021ac7 100644 --- a/src/sqlancer/mongodb/ast/MongoDBSelect.java +++ b/src/sqlancer/mongodb/ast/MongoDBSelect.java @@ -69,7 +69,7 @@ public Node getFilterClause() { return filterClause; } - public boolean hasWhere() { + public boolean hasFilter() { return hasFilter; } diff --git a/src/sqlancer/mongodb/ast/MongoDBUnaryLogicalOperatorNode.java b/src/sqlancer/mongodb/ast/MongoDBUnaryLogicalOperatorNode.java new file mode 100644 index 000000000..01b2b0499 --- /dev/null +++ b/src/sqlancer/mongodb/ast/MongoDBUnaryLogicalOperatorNode.java @@ -0,0 +1,16 @@ +package sqlancer.mongodb.ast; + +import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.gen.MongoDBExpressionGenerator.MongoDBUnaryLogicalOperator; + +public class MongoDBUnaryLogicalOperatorNode extends NewUnaryPrefixOperatorNode { + + public MongoDBUnaryLogicalOperatorNode(Node expr, MongoDBUnaryLogicalOperator op) { + super(expr, op); + } + + public MongoDBUnaryLogicalOperator operator() { + return (MongoDBUnaryLogicalOperator) op; + } +} diff --git a/src/sqlancer/mongodb/ast/MongoDBDummyPredicate.java b/src/sqlancer/mongodb/ast/MongoDBUnsupportedPredicate.java similarity index 53% rename from src/sqlancer/mongodb/ast/MongoDBDummyPredicate.java rename to src/sqlancer/mongodb/ast/MongoDBUnsupportedPredicate.java index a9ecc3325..eae143e7d 100644 --- a/src/sqlancer/mongodb/ast/MongoDBDummyPredicate.java +++ b/src/sqlancer/mongodb/ast/MongoDBUnsupportedPredicate.java @@ -2,6 +2,6 @@ import sqlancer.common.ast.newast.Node; -public class MongoDBDummyPredicate implements Node { +public class MongoDBUnsupportedPredicate implements Node { } diff --git a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java index 090016809..abcf522d9 100644 --- a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java @@ -3,6 +3,7 @@ import org.bson.Document; import sqlancer.Randomly; +import sqlancer.common.ast.newast.Node; import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; import sqlancer.mongodb.MongoDBSchema.MongoDBDataType; import sqlancer.mongodb.ast.MongoDBConstant; @@ -13,6 +14,7 @@ import sqlancer.mongodb.ast.MongoDBConstant.MongoDBNullConstant; import sqlancer.mongodb.ast.MongoDBConstant.MongoDBStringConstant; import sqlancer.mongodb.ast.MongoDBConstant.MongoDBTimestampConstant; +import sqlancer.mongodb.ast.MongoDBExpression; public class MongoDBConstantGenerator { private final MongoDBGlobalState globalState; @@ -21,6 +23,28 @@ public MongoDBConstantGenerator(MongoDBGlobalState globalState) { this.globalState = globalState; } + public Node generateConstantWithType(MongoDBDataType option) { + if (Randomly.getBooleanWithSmallProbability()) { + return MongoDBConstant.createNullConstant(); + } + switch (option) { + case DATE_TIME: + return MongoDBConstant.createDateTimeConstant(globalState.getRandomly().getInteger()); + case BOOLEAN: + return MongoDBConstant.createBooleanConstant(Randomly.getBoolean()); + case DOUBLE: + return MongoDBConstant.createDoubleConstant(globalState.getRandomly().getDouble()); + case STRING: + return MongoDBConstant.createStringConstant(globalState.getRandomly().getString()); + case INTEGER: + return MongoDBConstant.createIntegerConstant((int) globalState.getRandomly().getInteger()); + case TIMESTAMP: + return MongoDBConstant.createTimestampConstant(globalState.getRandomly().getInteger()); + default: + throw new AssertionError(option); + } + } + public void addRandomConstant(Document document, String key) { MongoDBDataType type = MongoDBDataType.getRandom(); addRandomConstantWithType(document, key, type); diff --git a/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java index f2c05dda2..597e2c3e8 100644 --- a/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java @@ -1,40 +1,227 @@ package sqlancer.mongodb.gen; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.bson.conversions.Bson; + +import com.mongodb.client.model.Filters; + +import sqlancer.Randomly; +import sqlancer.common.ast.BinaryOperatorNode.Operator; import sqlancer.common.ast.newast.Node; import sqlancer.common.gen.UntypedExpressionGenerator; -import sqlancer.mongodb.ast.MongoDBDummyPredicate; +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.MongoDBSchema.MongoDBDataType; +import sqlancer.mongodb.ast.MongoDBBinaryComparisonNode; +import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; +import sqlancer.mongodb.ast.MongoDBConstant; import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; +import sqlancer.mongodb.ast.MongoDBUnsupportedPredicate; import sqlancer.mongodb.test.MongoDBColumnTestReference; public class MongoDBExpressionGenerator extends UntypedExpressionGenerator, MongoDBColumnTestReference> { - /* - * private final MongoDBGlobalState globalState; - * - * public MongoDBExpressionGenerator(MongoDBGlobalState globalState) { this.globalState = globalState; } - */ + + private final MongoDBGlobalState globalState; + + private enum NonLeafExpression { + BINARY_LOGICAL, UNARY_LOGICAL + } + + public MongoDBExpressionGenerator(MongoDBGlobalState globalState) { + this.globalState = globalState; + } + @Override - public Node generateConstant() { - throw new UnsupportedOperationException(); + public Node generateLeafNode() { + MongoDBBinaryComparisonOperator operator = MongoDBBinaryComparisonOperator.getRandom(); + return new MongoDBBinaryComparisonNode(generateColumn(), generateConstant(), operator); } @Override protected Node generateExpression(int depth) { - return new MongoDBDummyPredicate<>(); + if (depth >= globalState.getOptions().getMaxExpressionDepth() || Randomly.getBoolean()) { + return generateLeafNode(); + } + + List possibleOptions = new ArrayList<>(Arrays.asList(NonLeafExpression.values())); + NonLeafExpression expr = Randomly.fromList(possibleOptions); + switch (expr) { + case BINARY_LOGICAL: + MongoDBBinaryLogicalOperator binaryOperator = MongoDBBinaryLogicalOperator.getRandom(); + return new MongoDBBinaryLogicalNode(generateExpression(depth + 1), generateExpression(depth + 1), + binaryOperator); + case UNARY_LOGICAL: + MongoDBUnaryLogicalOperator unaryOperator = MongoDBUnaryLogicalOperator.getRandom(); + return new MongoDBUnaryLogicalOperatorNode(generateExpression(depth + 1), unaryOperator); + default: + throw new AssertionError(); + } + } + + @Override + public Node generateConstant() { + MongoDBDataType type = MongoDBDataType.getRandom(); + MongoDBConstantGenerator generator = new MongoDBConstantGenerator(globalState); + return generator.generateConstantWithType(type); } @Override protected Node generateColumn() { - throw new UnsupportedOperationException(); + return Randomly.fromList(columns); } @Override public Node negatePredicate(Node predicate) { - return new MongoDBDummyPredicate<>(); + return new MongoDBUnaryLogicalOperatorNode(predicate, MongoDBUnaryLogicalOperator.NOT); } @Override public Node isNull(Node expr) { - return new MongoDBDummyPredicate<>(); + return new MongoDBUnsupportedPredicate<>(); + } + + public enum MongoDBUnaryLogicalOperator implements Operator { + NOT { + @Override + public Bson applyOperator(Bson inner) { + return Filters.nor(inner, Filters.exists("_id", false)); + } + + @Override + public String getTextRepresentation() { + return "{$nor: [{ _id: {$exists: false}}, "; + } + }; + + public abstract Bson applyOperator(Bson inner); + + public static MongoDBUnaryLogicalOperator getRandom() { + return Randomly.fromOptions(values()); + } + } + + public enum MongoDBBinaryLogicalOperator implements Operator { + AND { + @Override + public Bson applyOperator(Bson left, Bson right) { + return Filters.and(left, right); + } + + @Override + public String getTextRepresentation() { + return "$and"; + } + }, + OR { + @Override + public Bson applyOperator(Bson left, Bson right) { + return Filters.or(left, right); + } + + @Override + public String getTextRepresentation() { + return "$or"; + } + }, + NOR { + @Override + public Bson applyOperator(Bson left, Bson right) { + return Filters.nor(left, right); + } + + @Override + public String getTextRepresentation() { + return "$nor"; + } + }; + + public abstract Bson applyOperator(Bson left, Bson right); + + public static MongoDBBinaryLogicalOperator getRandom() { + return Randomly.fromOptions(values()); + } + } + + public enum MongoDBBinaryComparisonOperator implements Operator { + EQUALS { + @Override + public Bson applyOperator(String columnName, MongoDBConstant constant) { + return Filters.eq(columnName, constant.getValue()); + } + + @Override + public String getTextRepresentation() { + return "$eq"; + } + }, + NOT_EQUALS { + @Override + public Bson applyOperator(String columnName, MongoDBConstant constant) { + return Filters.ne(columnName, constant.getValue()); + } + + @Override + public String getTextRepresentation() { + return "$ne"; + } + }, + GREATER { + @Override + public Bson applyOperator(String columnName, MongoDBConstant constant) { + return Filters.gt(columnName, constant.getValue()); + } + + @Override + public String getTextRepresentation() { + return "$gt"; + } + + }, + LESS { + @Override + public Bson applyOperator(String columnName, MongoDBConstant constant) { + return Filters.lt(columnName, constant.getValue()); + } + + @Override + public String getTextRepresentation() { + return "$lt"; + } + + }, + GREATER_EQUAL { + @Override + public Bson applyOperator(String columnName, MongoDBConstant constant) { + return Filters.gte(columnName, constant.getValue()); + + } + + @Override + public String getTextRepresentation() { + return "$gte"; + } + + }, + LESS_EQUAL { + @Override + public Bson applyOperator(String columnName, MongoDBConstant constant) { + return Filters.lte(columnName, constant.getValue()); + } + + @Override + public String getTextRepresentation() { + return "$lte"; + } + }; + + public abstract Bson applyOperator(String columnName, MongoDBConstant constant); + + public static MongoDBBinaryComparisonOperator getRandom() { + return Randomly.fromOptions(values()); + } } } diff --git a/src/sqlancer/mongodb/test/MongoDBColumnTestReference.java b/src/sqlancer/mongodb/test/MongoDBColumnTestReference.java index f6c8e9d94..59a2a6724 100644 --- a/src/sqlancer/mongodb/test/MongoDBColumnTestReference.java +++ b/src/sqlancer/mongodb/test/MongoDBColumnTestReference.java @@ -1,8 +1,10 @@ package sqlancer.mongodb.test; +import sqlancer.common.ast.newast.Node; import sqlancer.mongodb.MongoDBSchema.MongoDBColumn; +import sqlancer.mongodb.ast.MongoDBExpression; -public class MongoDBColumnTestReference { +public class MongoDBColumnTestReference implements Node { private final MongoDBColumn columnReference; private final boolean inMainTable; @@ -31,4 +33,8 @@ public String getTableName() { public String getPlainName() { return this.columnReference.getName(); } + + public MongoDBColumn getColumnReference() { + return columnReference; + } } diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java index 4fda2a64b..c44cc3bed 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java @@ -37,7 +37,7 @@ public void check() throws Exception { targetTables = schema.getRandomTableNonEmptyTables(); mainTable = targetTables.getTables().get(0); generateTargetColumns(); - expressionGenerator = new MongoDBExpressionGenerator().setColumns(targetColumns); + expressionGenerator = new MongoDBExpressionGenerator(state).setColumns(targetColumns); initializeTernaryPredicateVariants(); select = new MongoDBSelect<>(mainTable.getName(), targetColumns.get(0)); select.setProjectionList(targetColumns); diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java index 11ae2c8ae..8c9522aaa 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java @@ -1,5 +1,10 @@ package sqlancer.mongodb.test; +import java.util.List; + +import org.bson.Document; + +import sqlancer.mongodb.MongoDBComparatorHelper; import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; import sqlancer.mongodb.query.MongoDBSelectQuery; @@ -15,17 +20,20 @@ public void check() throws Exception { select.setFilterClause(null); MongoDBSelectQuery q = new MongoDBSelectQuery(select); q.executeAndGet(state); - /* - * List firstResultSet = q.getResultSet(); select.setWhereClause(predicate); q = new - * MongoDBFindQueryAdapter(select, mainTable); q.executeAndGet(state); List secondResultSet = - * q.resultSet; - * - * select.setWhereClause(negatedPredicate); q = new MongoDBFindQueryAdapter(select, mainTable); - * q.executeAndGet(state); List thirdResultSet = q.resultSet; - * - * secondResultSet.addAll(thirdResultSet); MongoDBComparatorHelper.assumeResultSetsAreEqual(firstResultSet, - * secondResultSet, q); - * - */ + + List firstResultSet = q.getResultSet(); + select.setFilterClause(predicate); + q = new MongoDBSelectQuery(select); + q.executeAndGet(state); + List secondResultSet = q.getResultSet(); + + select.setFilterClause(negatedPredicate); + q = new MongoDBSelectQuery(select); + q.executeAndGet(state); + List thirdResultSet = q.getResultSet(); + + secondResultSet.addAll(thirdResultSet); + MongoDBComparatorHelper.assumeResultSetsAreEqual(firstResultSet, secondResultSet, q); + } } diff --git a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java index 50d43a3ee..4d68ac09e 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java @@ -3,16 +3,56 @@ import java.util.ArrayList; import java.util.List; +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.ast.MongoDBBinaryComparisonNode; +import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; import sqlancer.mongodb.ast.MongoDBConstant; import sqlancer.mongodb.ast.MongoDBExpression; import sqlancer.mongodb.ast.MongoDBSelect; +import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; import sqlancer.mongodb.test.MongoDBColumnTestReference; public class MongoDBToLogVisitor extends MongoDBVisitor { private String mainTableName; private List lookups; + private String filter; private String projects; + private boolean hasFilter; + + public String visitLog(Node expr) { + if (expr instanceof MongoDBUnaryLogicalOperatorNode) { + return visit((MongoDBUnaryLogicalOperatorNode) expr); + } else if (expr instanceof MongoDBBinaryLogicalNode) { + return visit((MongoDBBinaryLogicalNode) expr); + } else if (expr instanceof MongoDBBinaryComparisonNode) { + return visit((MongoDBBinaryComparisonNode) expr); + } else { + throw new AssertionError(expr.getClass()); + } + } + + public String visit(MongoDBUnaryLogicalOperatorNode expr) { + String inner = visitLog(expr.getExpr()); + return expr.operator().getTextRepresentation() + inner + "]}"; + } + + public String visit(MongoDBBinaryLogicalNode expr) { + String left = visitLog(expr.getLeft()); + String right = visitLog(expr.getRight()); + + return "{" + expr.operator().getTextRepresentation() + ":[" + left + "," + right + "]}"; + } + + public String visit(MongoDBBinaryComparisonNode expr) { + Node left = expr.getLeft(); + Node right = expr.getRight(); + assert left instanceof MongoDBColumnTestReference; + assert right instanceof MongoDBConstant; + + return "{\"" + ((MongoDBColumnTestReference) left).getQueryString() + "\": {" + + expr.operator().getTextRepresentation() + ": " + ((MongoDBConstant) right).getLogValue() + "}}"; + } @Override public void visit(MongoDBConstant c) { @@ -21,11 +61,19 @@ public void visit(MongoDBConstant c) { @Override public void visit(MongoDBSelect select) { + hasFilter = select.hasFilter(); mainTableName = select.getMainTableName(); setLookups(select); + if (hasFilter) { + setFilter(select); + } setProjects(select); } + private void setFilter(MongoDBSelect select) { + filter = visitLog(select.getFilterClause()); + } + private void setLookups(MongoDBSelect select) { lookups = new ArrayList<>(); for (MongoDBColumnTestReference testReference : select.getLookupList()) { @@ -58,12 +106,11 @@ public String getStringLog() { for (String lookup : lookups) { sb.append(lookup); } - // sb.append(",\n"); - // if(select.hasWhere()) { - // sb.append("{ $match: "); - // sb.append(mongoDBToQueryVisitor.getFilterLog()); - // sb.append("},\n"); - // } + if (hasFilter) { + sb.append("{ $match: "); + sb.append(filter); + sb.append("},\n"); + } sb.append("{ $project : "); sb.append(projects); sb.append("}])\n"); diff --git a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java index 3ad57a3cb..25fd94dbf 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java @@ -1,5 +1,6 @@ package sqlancer.mongodb.visitor; +import static com.mongodb.client.model.Aggregates.match; import static com.mongodb.client.model.Aggregates.project; import static com.mongodb.client.model.Projections.fields; import static com.mongodb.client.model.Projections.include; @@ -11,15 +12,54 @@ import com.mongodb.client.model.Aggregates; +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.ast.MongoDBBinaryComparisonNode; +import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; import sqlancer.mongodb.ast.MongoDBConstant; import sqlancer.mongodb.ast.MongoDBExpression; import sqlancer.mongodb.ast.MongoDBSelect; +import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; import sqlancer.mongodb.test.MongoDBColumnTestReference; public class MongoDBToQueryVisitor extends MongoDBVisitor { private List lookup; + private Bson filter; private Bson projection; + private boolean hasFilter; + + public Bson visitBson(Node expr) { + if (expr instanceof MongoDBUnaryLogicalOperatorNode) { + return visit((MongoDBUnaryLogicalOperatorNode) expr); + } else if (expr instanceof MongoDBBinaryLogicalNode) { + return visit((MongoDBBinaryLogicalNode) expr); + } else if (expr instanceof MongoDBBinaryComparisonNode) { + return visit((MongoDBBinaryComparisonNode) expr); + } else { + throw new AssertionError(expr.getClass()); + } + } + + public Bson visit(MongoDBUnaryLogicalOperatorNode expr) { + Bson inner = visitBson(expr.getExpr()); + return expr.operator().applyOperator(inner); + } + + public Bson visit(MongoDBBinaryLogicalNode expr) { + Bson left = visitBson(expr.getLeft()); + Bson right = visitBson(expr.getRight()); + return expr.operator().applyOperator(left, right); + } + + public Bson visit(MongoDBBinaryComparisonNode expr) { + Node left = expr.getLeft(); + Node right = expr.getRight(); + assert left instanceof MongoDBColumnTestReference; + assert right instanceof MongoDBConstant; + + String columnName = ((MongoDBColumnTestReference) left).getQueryString(); + return expr.operator().applyOperator(columnName, (MongoDBConstant) right); + } @Override public void visit(MongoDBConstant c) { @@ -28,13 +68,18 @@ public void visit(MongoDBConstant c) { @Override public void visit(MongoDBSelect select) { + hasFilter = select.hasFilter(); setLookup(select); - // if(select.hasWhere()) { - // // DO STUFF - // } + if (hasFilter) { + setFilter(select); + } setProjection(select); } + private void setFilter(MongoDBSelect select) { + filter = match(this.visitBson(select.getFilterClause())); + } + private void setLookup(MongoDBSelect select) { lookup = new ArrayList<>(); for (MongoDBColumnTestReference reference : select.getLookupList()) { @@ -56,7 +101,9 @@ private void setProjection(MongoDBSelect select) { public List getPipeline() { List result = new ArrayList<>(lookup); - // TODO Patrick: Add Match if where... + if (hasFilter) { + result.add(filter); + } result.add(projection); return result; } diff --git a/src/sqlancer/mongodb/visitor/MongoDBVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBVisitor.java index 8c207a564..67b6ca741 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBVisitor.java @@ -10,6 +10,7 @@ import sqlancer.mongodb.ast.MongoDBSelect; public abstract class MongoDBVisitor { + public abstract void visit(MongoDBConstant c); public abstract void visit(MongoDBSelect s); From 1cd7c47c038cd60f343988049f31223e0f4031c1 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 26 Jan 2021 13:29:54 +0100 Subject: [PATCH 05/24] Expand expression generation to computed functions for MongoDB Computed functions allows to perform arithmetic operations in queries. It is part of the projection pipeline stage, where a new field is added that holds the result. This update adds support for random computed fields with functions such as add, multiply, pow, sqrt and more. --- src/sqlancer/mongodb/MongoDBOptions.java | 3 + .../ast/MongoDBBinaryComparisonNode.java | 2 +- .../mongodb/ast/MongoDBBinaryLogicalNode.java | 2 +- src/sqlancer/mongodb/ast/MongoDBConstant.java | 39 ++++++++ src/sqlancer/mongodb/ast/MongoDBSelect.java | 19 ++++ .../ast/MongoDBUnaryLogicalOperatorNode.java | 2 +- .../MongoDBComputedExpressionGenerator.java | 89 +++++++++++++++++++ ...a => MongoDBMatchExpressionGenerator.java} | 4 +- .../test/MongoDBQueryPartitioningBase.java | 21 ++++- .../visitor/MongoDBToQueryVisitor.java | 50 ++++++++++- 10 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 src/sqlancer/mongodb/gen/MongoDBComputedExpressionGenerator.java rename src/sqlancer/mongodb/gen/{MongoDBExpressionGenerator.java => MongoDBMatchExpressionGenerator.java} (98%) diff --git a/src/sqlancer/mongodb/MongoDBOptions.java b/src/sqlancer/mongodb/MongoDBOptions.java index a6306b515..af598c34e 100644 --- a/src/sqlancer/mongodb/MongoDBOptions.java +++ b/src/sqlancer/mongodb/MongoDBOptions.java @@ -28,6 +28,9 @@ public class MongoDBOptions implements DBMSSpecificOptions oracles = Arrays.asList(QUERY_PARTITIONING); diff --git a/src/sqlancer/mongodb/ast/MongoDBBinaryComparisonNode.java b/src/sqlancer/mongodb/ast/MongoDBBinaryComparisonNode.java index f5e3ea173..21675250a 100644 --- a/src/sqlancer/mongodb/ast/MongoDBBinaryComparisonNode.java +++ b/src/sqlancer/mongodb/ast/MongoDBBinaryComparisonNode.java @@ -2,7 +2,7 @@ import sqlancer.common.ast.newast.NewBinaryOperatorNode; import sqlancer.common.ast.newast.Node; -import sqlancer.mongodb.gen.MongoDBExpressionGenerator.MongoDBBinaryComparisonOperator; +import sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator; public class MongoDBBinaryComparisonNode extends NewBinaryOperatorNode { public MongoDBBinaryComparisonNode(Node left, Node right, diff --git a/src/sqlancer/mongodb/ast/MongoDBBinaryLogicalNode.java b/src/sqlancer/mongodb/ast/MongoDBBinaryLogicalNode.java index c3f18a248..efb8d8294 100644 --- a/src/sqlancer/mongodb/ast/MongoDBBinaryLogicalNode.java +++ b/src/sqlancer/mongodb/ast/MongoDBBinaryLogicalNode.java @@ -2,7 +2,7 @@ import sqlancer.common.ast.newast.NewBinaryOperatorNode; import sqlancer.common.ast.newast.Node; -import sqlancer.mongodb.gen.MongoDBExpressionGenerator.MongoDBBinaryLogicalOperator; +import sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBBinaryLogicalOperator; public class MongoDBBinaryLogicalNode extends NewBinaryOperatorNode { public MongoDBBinaryLogicalNode(Node left, Node right, diff --git a/src/sqlancer/mongodb/ast/MongoDBConstant.java b/src/sqlancer/mongodb/ast/MongoDBConstant.java index f52c18666..86f783b48 100644 --- a/src/sqlancer/mongodb/ast/MongoDBConstant.java +++ b/src/sqlancer/mongodb/ast/MongoDBConstant.java @@ -1,5 +1,7 @@ package sqlancer.mongodb.ast; +import java.io.Serializable; + import org.bson.BsonDateTime; import org.bson.BsonTimestamp; import org.bson.Document; @@ -16,6 +18,8 @@ private MongoDBConstant() { public abstract Object getValue(); + public abstract Serializable getSerializedValue(); + public static class MongoDBNullConstant extends MongoDBConstant { @Override @@ -32,6 +36,11 @@ public String getLogValue() { public Object getValue() { return null; } + + @Override + public Serializable getSerializedValue() { + return null; + } } public static Node createNullConstant() { @@ -60,6 +69,11 @@ public String getLogValue() { public Integer getValue() { return value; } + + @Override + public Serializable getSerializedValue() { + return value; + } } public static Node createIntegerConstant(int value) { @@ -92,6 +106,11 @@ public String getLogValue() { public String getValue() { return value; } + + @Override + public Serializable getSerializedValue() { + return value; + } } public static Node createStringConstant(String value) { @@ -120,6 +139,11 @@ public String getLogValue() { public Boolean getValue() { return value; } + + @Override + public Serializable getSerializedValue() { + return value; + } } public static Node createBooleanConstant(boolean value) { @@ -148,6 +172,11 @@ public String getLogValue() { public Double getValue() { return value; } + + @Override + public Serializable getSerializedValue() { + return value; + } } public static Node createDoubleConstant(double value) { @@ -176,6 +205,11 @@ public String getLogValue() { public BsonDateTime getValue() { return value; } + + @Override + public Serializable getSerializedValue() { + return value.getValue(); + } } public static Node createDateTimeConstant(long value) { @@ -204,6 +238,11 @@ public String getLogValue() { public BsonTimestamp getValue() { return value; } + + @Override + public Serializable getSerializedValue() { + return value.getValue(); + } } public static Node createTimestampConstant(long value) { diff --git a/src/sqlancer/mongodb/ast/MongoDBSelect.java b/src/sqlancer/mongodb/ast/MongoDBSelect.java index 39b021ac7..de65fb01d 100644 --- a/src/sqlancer/mongodb/ast/MongoDBSelect.java +++ b/src/sqlancer/mongodb/ast/MongoDBSelect.java @@ -13,6 +13,8 @@ public class MongoDBSelect implements Node { List lookupList; boolean hasFilter; Node filterClause; + boolean hasComputed; + List> computedClauses; public MongoDBSelect(String mainTableName, MongoDBColumnTestReference joinColumn) { this.mainTableName = mainTableName; @@ -73,4 +75,21 @@ public boolean hasFilter() { return hasFilter; } + public void setComputedClause(List> computedClause) { + if (computedClause == null) { + hasComputed = false; + this.computedClauses = null; + return; + } + hasComputed = true; + this.computedClauses = computedClause; + } + + public List> getComputedClause() { + return computedClauses; + } + + public boolean hasComputed() { + return hasComputed; + } } diff --git a/src/sqlancer/mongodb/ast/MongoDBUnaryLogicalOperatorNode.java b/src/sqlancer/mongodb/ast/MongoDBUnaryLogicalOperatorNode.java index 01b2b0499..a34fe27e5 100644 --- a/src/sqlancer/mongodb/ast/MongoDBUnaryLogicalOperatorNode.java +++ b/src/sqlancer/mongodb/ast/MongoDBUnaryLogicalOperatorNode.java @@ -2,7 +2,7 @@ import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; import sqlancer.common.ast.newast.Node; -import sqlancer.mongodb.gen.MongoDBExpressionGenerator.MongoDBUnaryLogicalOperator; +import sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBUnaryLogicalOperator; public class MongoDBUnaryLogicalOperatorNode extends NewUnaryPrefixOperatorNode { diff --git a/src/sqlancer/mongodb/gen/MongoDBComputedExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBComputedExpressionGenerator.java new file mode 100644 index 000000000..1090b3f2f --- /dev/null +++ b/src/sqlancer/mongodb/gen/MongoDBComputedExpressionGenerator.java @@ -0,0 +1,89 @@ +package sqlancer.mongodb.gen; + +import java.util.ArrayList; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.common.ast.newast.NewFunctionNode; +import sqlancer.common.ast.newast.Node; +import sqlancer.common.gen.UntypedExpressionGenerator; +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; +import sqlancer.mongodb.MongoDBSchema; +import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.test.MongoDBColumnTestReference; + +public class MongoDBComputedExpressionGenerator + extends UntypedExpressionGenerator, MongoDBColumnTestReference> { + + private final MongoDBGlobalState globalState; + + @Override + public Node generateLeafNode() { + ComputedFunction function = ComputedFunction.getRandom(); + List> expressions = new ArrayList<>(); + for (int i = 0; i < function.getNrArgs(); i++) { + expressions.add(super.generateLeafNode()); + } + return new NewFunctionNode<>(expressions, function); + } + + @Override + protected Node generateExpression(int depth) { + if (depth >= globalState.getOptions().getMaxExpressionDepth() || Randomly.getBoolean()) { + return generateLeafNode(); + } + ComputedFunction func = ComputedFunction.getRandom(); + return new NewFunctionNode<>(generateExpressions(depth + 1, func.getNrArgs()), func); + } + + public MongoDBComputedExpressionGenerator(MongoDBGlobalState globalState) { + this.globalState = globalState; + } + + public enum ComputedFunction { + ADD(2, "$add"), MULTIPLY(2, "$multiply"), DIVIDE(2, "$divide"), POW(2, "$pow"), SQRT(1, "$sqrt"), + LOG(2, "$log"), AVG(2, "$avg"), EXP(1, "$exp"); + + private final int nrArgs; + private final String operatorName; + + ComputedFunction(int nrArgs, String operatorName) { + this.nrArgs = nrArgs; + this.operatorName = operatorName; + } + + public static ComputedFunction getRandom() { + return Randomly.fromOptions(values()); + } + + public int getNrArgs() { + return nrArgs; + } + + public String getOperator() { + return operatorName; + } + } + + @Override + public Node generateConstant() { + MongoDBSchema.MongoDBDataType type = MongoDBSchema.MongoDBDataType.getRandom(); + MongoDBConstantGenerator generator = new MongoDBConstantGenerator(globalState); + return generator.generateConstantWithType(type); + } + + @Override + protected Node generateColumn() { + return Randomly.fromList(columns); + } + + @Override + public Node negatePredicate(Node predicate) { + throw new UnsupportedOperationException(); + } + + @Override + public Node isNull(Node expr) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java similarity index 98% rename from src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java rename to src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java index 597e2c3e8..df6791794 100644 --- a/src/sqlancer/mongodb/gen/MongoDBExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java @@ -22,7 +22,7 @@ import sqlancer.mongodb.ast.MongoDBUnsupportedPredicate; import sqlancer.mongodb.test.MongoDBColumnTestReference; -public class MongoDBExpressionGenerator +public class MongoDBMatchExpressionGenerator extends UntypedExpressionGenerator, MongoDBColumnTestReference> { private final MongoDBGlobalState globalState; @@ -31,7 +31,7 @@ private enum NonLeafExpression { BINARY_LOGICAL, UNARY_LOGICAL } - public MongoDBExpressionGenerator(MongoDBGlobalState globalState) { + public MongoDBMatchExpressionGenerator(MongoDBGlobalState globalState) { this.globalState = globalState; } diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java index c44cc3bed..c33bcf9e6 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java @@ -15,7 +15,8 @@ import sqlancer.mongodb.MongoDBSchema.MongoDBTables; import sqlancer.mongodb.ast.MongoDBExpression; import sqlancer.mongodb.ast.MongoDBSelect; -import sqlancer.mongodb.gen.MongoDBExpressionGenerator; +import sqlancer.mongodb.gen.MongoDBComputedExpressionGenerator; +import sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator; public class MongoDBQueryPartitioningBase extends TernaryLogicPartitioningOracleBase, MongoDBGlobalState> implements TestOracle { @@ -24,7 +25,7 @@ public class MongoDBQueryPartitioningBase protected MongoDBTables targetTables; protected MongoDBTable mainTable; protected List targetColumns; - protected MongoDBExpressionGenerator expressionGenerator; + protected MongoDBMatchExpressionGenerator expressionGenerator; protected MongoDBSelect select; public MongoDBQueryPartitioningBase(MongoDBGlobalState state) { @@ -37,7 +38,7 @@ public void check() throws Exception { targetTables = schema.getRandomTableNonEmptyTables(); mainTable = targetTables.getTables().get(0); generateTargetColumns(); - expressionGenerator = new MongoDBExpressionGenerator(state).setColumns(targetColumns); + expressionGenerator = new MongoDBMatchExpressionGenerator(state).setColumns(targetColumns); initializeTernaryPredicateVariants(); select = new MongoDBSelect<>(mainTable.getName(), targetColumns.get(0)); select.setProjectionList(targetColumns); @@ -46,6 +47,20 @@ public void check() throws Exception { } else { select.setLookupList(Randomly.nonEmptySubset(targetColumns)); } + if (state.getDmbsSpecificOptions().testComputedValues) { + generateComputedColumns(); + } + } + + private void generateComputedColumns() { + List> computedColumns = new ArrayList<>(); + int numberComputedColumns = state.getRandomly().getInteger(1, 4); + MongoDBComputedExpressionGenerator generator = new MongoDBComputedExpressionGenerator(state) + .setColumns(targetColumns); + for (int i = 0; i < numberComputedColumns; i++) { + computedColumns.add(generator.generateExpression()); + } + select.setComputedClause(computedColumns); } private void generateTargetColumns() { diff --git a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java index 25fd94dbf..1ebeee84e 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java @@ -5,13 +5,17 @@ import static com.mongodb.client.model.Projections.fields; import static com.mongodb.client.model.Projections.include; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import org.bson.Document; import org.bson.conversions.Bson; import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections; +import sqlancer.common.ast.newast.NewFunctionNode; import sqlancer.common.ast.newast.Node; import sqlancer.mongodb.ast.MongoDBBinaryComparisonNode; import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; @@ -19,6 +23,7 @@ import sqlancer.mongodb.ast.MongoDBExpression; import sqlancer.mongodb.ast.MongoDBSelect; import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; +import sqlancer.mongodb.gen.MongoDBComputedExpressionGenerator.ComputedFunction; import sqlancer.mongodb.test.MongoDBColumnTestReference; public class MongoDBToQueryVisitor extends MongoDBVisitor { @@ -40,6 +45,39 @@ public Bson visitBson(Node expr) { } } + public Document visitComputed(Node expr) { + if (expr instanceof NewFunctionNode) { + return visitComputed((NewFunctionNode) expr); + } else { + throw new AssertionError(expr.getClass()); + } + } + + public Document visitComputed(NewFunctionNode expr) { + List visitedArgs = new ArrayList<>(); + for (int i = 0; i < expr.getArgs().size(); i++) { + if (expr.getArgs().get(i) instanceof MongoDBConstant) { + visitedArgs.add(((MongoDBConstant) expr.getArgs().get(i)).getSerializedValue()); + continue; + } + if (expr.getArgs().get(i) instanceof MongoDBColumnTestReference) { + visitedArgs.add("$" + ((MongoDBColumnTestReference) expr.getArgs().get(i)).getQueryString()); + continue; + } + if (expr.getArgs().get(i) instanceof NewFunctionNode) { + visitedArgs.add(visitComputed((NewFunctionNode) expr.getArgs().get(i))); + } else { + throw new AssertionError(); + } + } + if (expr.getFunc() instanceof ComputedFunction) { + return new Document(((ComputedFunction) expr.getFunc()).getOperator(), visitedArgs); + } else { + throw new AssertionError(expr.getClass()); + } + + } + public Bson visit(MongoDBUnaryLogicalOperatorNode expr) { Bson inner = visitBson(expr.getExpr()); return expr.operator().applyOperator(inner); @@ -96,7 +134,17 @@ private void setProjection(MongoDBSelect select) { for (MongoDBColumnTestReference ref : select.getProjectionList()) { stringProjects.add(ref.getQueryString()); } - projection = project(fields(include(stringProjects))); + List projections = new ArrayList<>(); + projections.add(include(stringProjects)); + if (select.hasComputed()) { + String name = "computed"; + int number = 0; + for (Node expressionNode : select.getComputedClause()) { + projections.add(Projections.computed(name + number, visitComputed(expressionNode))); + number++; + } + } + projection = project(fields(projections)); } public List getPipeline() { From 147ce91db809ec8e196f5edd4cf7d394fbfb88e6 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 26 Jan 2021 15:01:22 +0100 Subject: [PATCH 06/24] Add regular expressions as part of the randomized expression generation in MongoDB Similar to binary comparison regular expression is a new Leaf node that can be added in the tree. To generate the pattern the existing random string generator is used. --- src/sqlancer/mongodb/MongoDBOptions.java | 3 ++ .../mongodb/ast/MongoDBRegexNode.java | 17 ++++++++ .../gen/MongoDBMatchExpressionGenerator.java | 39 ++++++++++++++++++- .../mongodb/visitor/MongoDBToLogVisitor.java | 14 +++++++ .../visitor/MongoDBToQueryVisitor.java | 15 +++++++ 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/sqlancer/mongodb/ast/MongoDBRegexNode.java diff --git a/src/sqlancer/mongodb/MongoDBOptions.java b/src/sqlancer/mongodb/MongoDBOptions.java index af598c34e..5c16bfd4d 100644 --- a/src/sqlancer/mongodb/MongoDBOptions.java +++ b/src/sqlancer/mongodb/MongoDBOptions.java @@ -31,6 +31,9 @@ public class MongoDBOptions implements DBMSSpecificOptions oracles = Arrays.asList(QUERY_PARTITIONING); diff --git a/src/sqlancer/mongodb/ast/MongoDBRegexNode.java b/src/sqlancer/mongodb/ast/MongoDBRegexNode.java new file mode 100644 index 000000000..154ebeffa --- /dev/null +++ b/src/sqlancer/mongodb/ast/MongoDBRegexNode.java @@ -0,0 +1,17 @@ +package sqlancer.mongodb.ast; + +import static sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBRegexOperator.REGEX; + +import sqlancer.common.ast.newast.NewBinaryOperatorNode; +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBRegexOperator; + +public class MongoDBRegexNode extends NewBinaryOperatorNode { + public MongoDBRegexNode(Node left, Node right) { + super(left, right, REGEX); + } + + public MongoDBRegexOperator operator() { + return (MongoDBRegexOperator) op; + } +} diff --git a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java index df6791794..272a52d30 100644 --- a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java @@ -18,6 +18,7 @@ import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; import sqlancer.mongodb.ast.MongoDBConstant; import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBRegexNode; import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; import sqlancer.mongodb.ast.MongoDBUnsupportedPredicate; import sqlancer.mongodb.test.MongoDBColumnTestReference; @@ -27,6 +28,11 @@ public class MongoDBMatchExpressionGenerator private final MongoDBGlobalState globalState; + private enum LeafExpression { + BINARY_COMPARISON, REGEX + + } + private enum NonLeafExpression { BINARY_LOGICAL, UNARY_LOGICAL } @@ -37,8 +43,21 @@ public MongoDBMatchExpressionGenerator(MongoDBGlobalState globalState) { @Override public Node generateLeafNode() { - MongoDBBinaryComparisonOperator operator = MongoDBBinaryComparisonOperator.getRandom(); - return new MongoDBBinaryComparisonNode(generateColumn(), generateConstant(), operator); + List possibleOptions = new ArrayList<>(Arrays.asList(LeafExpression.values())); + if (!globalState.getDmbsSpecificOptions().testWithRegex) { + possibleOptions.remove(LeafExpression.REGEX); + } + LeafExpression expr = Randomly.fromList(possibleOptions); + switch (expr) { + case BINARY_COMPARISON: + MongoDBBinaryComparisonOperator operator = MongoDBBinaryComparisonOperator.getRandom(); + return new MongoDBBinaryComparisonNode(generateColumn(), generateConstant(), operator); + case REGEX: + return new MongoDBRegexNode(generateColumn(), + new MongoDBConstantGenerator(globalState).generateConstantWithType(MongoDBDataType.STRING)); + default: + throw new AssertionError(); + } } @Override @@ -224,4 +243,20 @@ public static MongoDBBinaryComparisonOperator getRandom() { return Randomly.fromOptions(values()); } } + + public enum MongoDBRegexOperator implements Operator { + REGEX { + @Override + public Bson applyOperator(String columnName, MongoDBConstant.MongoDBStringConstant regex) { + return Filters.regex(columnName, regex.getStringValue(), ""); + } + + @Override + public String getTextRepresentation() { + return "$regex"; + } + }; + + public abstract Bson applyOperator(String columnName, MongoDBConstant.MongoDBStringConstant regex); + } } diff --git a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java index 4d68ac09e..bef5e5952 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java @@ -8,6 +8,7 @@ import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; import sqlancer.mongodb.ast.MongoDBConstant; import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBRegexNode; import sqlancer.mongodb.ast.MongoDBSelect; import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; import sqlancer.mongodb.test.MongoDBColumnTestReference; @@ -27,6 +28,8 @@ public String visitLog(Node expr) { return visit((MongoDBBinaryLogicalNode) expr); } else if (expr instanceof MongoDBBinaryComparisonNode) { return visit((MongoDBBinaryComparisonNode) expr); + } else if (expr instanceof MongoDBRegexNode) { + return visit((MongoDBRegexNode) expr); } else { throw new AssertionError(expr.getClass()); } @@ -54,6 +57,17 @@ public String visit(MongoDBBinaryComparisonNode expr) { + expr.operator().getTextRepresentation() + ": " + ((MongoDBConstant) right).getLogValue() + "}}"; } + public String visit(MongoDBRegexNode expr) { + Node left = expr.getLeft(); + Node right = expr.getRight(); + assert left instanceof MongoDBColumnTestReference; + assert right instanceof MongoDBConstant.MongoDBStringConstant; + + return "{\"" + ((MongoDBColumnTestReference) left).getQueryString() + "\": {" + + expr.operator().getTextRepresentation() + ": \'" + + ((MongoDBConstant.MongoDBStringConstant) right).getStringValue() + "\', $options: \'\'}}"; + } + @Override public void visit(MongoDBConstant c) { throw new UnsupportedOperationException(); diff --git a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java index 1ebeee84e..fb027add6 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java @@ -20,7 +20,9 @@ import sqlancer.mongodb.ast.MongoDBBinaryComparisonNode; import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; import sqlancer.mongodb.ast.MongoDBConstant; +import sqlancer.mongodb.ast.MongoDBConstant.MongoDBStringConstant; import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBRegexNode; import sqlancer.mongodb.ast.MongoDBSelect; import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; import sqlancer.mongodb.gen.MongoDBComputedExpressionGenerator.ComputedFunction; @@ -40,6 +42,8 @@ public Bson visitBson(Node expr) { return visit((MongoDBBinaryLogicalNode) expr); } else if (expr instanceof MongoDBBinaryComparisonNode) { return visit((MongoDBBinaryComparisonNode) expr); + } else if (expr instanceof MongoDBRegexNode) { + return visit((MongoDBRegexNode) expr); } else { throw new AssertionError(expr.getClass()); } @@ -89,6 +93,17 @@ public Bson visit(MongoDBBinaryLogicalNode expr) { return expr.operator().applyOperator(left, right); } + public Bson visit(MongoDBRegexNode expr) { + Node left = expr.getLeft(); + Node right = expr.getRight(); + + assert left instanceof MongoDBColumnTestReference; + assert right instanceof MongoDBStringConstant; + + String columnName = ((MongoDBColumnTestReference) left).getQueryString(); + return expr.operator().applyOperator(columnName, (MongoDBStringConstant) right); + } + public Bson visit(MongoDBBinaryComparisonNode expr) { Node left = expr.getLeft(); Node right = expr.getRight(); From 5d280448a932db6eba83d1bb7572846bdce36418 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 31 Jan 2021 01:37:01 +0100 Subject: [PATCH 07/24] Ignore expected errors in MongoDB queries and add expected errors for regular expression Changes to the MongoDBComparatorHelper allow for expected errors to be ignored if they are thrown in a query execution. This feature is mainly necessary for all the illformed regular expressions that the randomized string produces. --- .../mongodb/MongoDBComparatorHelper.java | 28 ++++++++++++ src/sqlancer/mongodb/MongoDBQueryAdapter.java | 6 --- .../mongodb/gen/MongoDBConstantGenerator.java | 3 -- .../gen/MongoDBMatchExpressionGenerator.java | 4 +- .../query/MongoDBCreateIndexQuery.java | 6 +++ .../query/MongoDBCreateTableQuery.java | 6 +++ .../mongodb/query/MongoDBInsertQuery.java | 6 +++ .../mongodb/query/MongoDBSelectQuery.java | 43 +++++++++++++++++++ .../MongoDBQueryPartitioningWhereTester.java | 10 ++--- .../mongodb/visitor/MongoDBToLogVisitor.java | 2 - .../visitor/MongoDBToQueryVisitor.java | 4 +- 11 files changed, 98 insertions(+), 20 deletions(-) diff --git a/src/sqlancer/mongodb/MongoDBComparatorHelper.java b/src/sqlancer/mongodb/MongoDBComparatorHelper.java index 235e5ece9..02996cfda 100644 --- a/src/sqlancer/mongodb/MongoDBComparatorHelper.java +++ b/src/sqlancer/mongodb/MongoDBComparatorHelper.java @@ -6,6 +6,9 @@ import org.bson.Document; +import sqlancer.IgnoreMeException; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; import sqlancer.mongodb.query.MongoDBSelectQuery; public final class MongoDBComparatorHelper { @@ -13,6 +16,31 @@ public final class MongoDBComparatorHelper { private MongoDBComparatorHelper() { } + public static List getResultSetAsDocumentList(MongoDBSelectQuery adapter, MongoDBGlobalState state) + throws Exception { + ExpectedErrors errors = adapter.getExpectedErrors(); + List result; + try { + adapter.executeAndGet(state); + result = adapter.getResultSet(); + return result; + + } catch (Exception e) { + if (e instanceof IgnoreMeException) { + throw e; + } + + if (e.getMessage() == null) { + throw new AssertionError(adapter.getLogString(), e); + } + + if (errors.errorIsExpected(e.getMessage())) { + throw new IgnoreMeException(); + } + throw new AssertionError(adapter.getLogString(), e); + } + } + public static void assumeResultSetsAreEqual(List resultSet, List secondResultSet, MongoDBSelectQuery originalQuery) { if (resultSet.size() != secondResultSet.size()) { diff --git a/src/sqlancer/mongodb/MongoDBQueryAdapter.java b/src/sqlancer/mongodb/MongoDBQueryAdapter.java index 438163486..e2add3242 100644 --- a/src/sqlancer/mongodb/MongoDBQueryAdapter.java +++ b/src/sqlancer/mongodb/MongoDBQueryAdapter.java @@ -1,6 +1,5 @@ package sqlancer.mongodb; -import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.Query; public abstract class MongoDBQueryAdapter extends Query { @@ -13,9 +12,4 @@ public String getQueryString() { public String getUnterminatedQueryString() { throw new UnsupportedOperationException(); } - - @Override - public ExpectedErrors getExpectedErrors() { - return new ExpectedErrors(); - } } diff --git a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java index abcf522d9..c0a8bdb77 100644 --- a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java @@ -24,9 +24,6 @@ public MongoDBConstantGenerator(MongoDBGlobalState globalState) { } public Node generateConstantWithType(MongoDBDataType option) { - if (Randomly.getBooleanWithSmallProbability()) { - return MongoDBConstant.createNullConstant(); - } switch (option) { case DATE_TIME: return MongoDBConstant.createDateTimeConstant(globalState.getRandomly().getInteger()); diff --git a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java index 272a52d30..e11de1165 100644 --- a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java @@ -30,7 +30,6 @@ public class MongoDBMatchExpressionGenerator private enum LeafExpression { BINARY_COMPARISON, REGEX - } private enum NonLeafExpression { @@ -85,6 +84,9 @@ protected Node generateExpression(int depth) { public Node generateConstant() { MongoDBDataType type = MongoDBDataType.getRandom(); MongoDBConstantGenerator generator = new MongoDBConstantGenerator(globalState); + if (Randomly.getBooleanWithSmallProbability()) { + return MongoDBConstant.createNullConstant(); + } return generator.generateConstantWithType(type); } diff --git a/src/sqlancer/mongodb/query/MongoDBCreateIndexQuery.java b/src/sqlancer/mongodb/query/MongoDBCreateIndexQuery.java index 8d54d9cf7..c873b5924 100644 --- a/src/sqlancer/mongodb/query/MongoDBCreateIndexQuery.java +++ b/src/sqlancer/mongodb/query/MongoDBCreateIndexQuery.java @@ -9,6 +9,7 @@ import sqlancer.GlobalState; import sqlancer.Main; +import sqlancer.common.query.ExpectedErrors; import sqlancer.mongodb.MongoDBConnection; import sqlancer.mongodb.MongoDBQueryAdapter; import sqlancer.mongodb.MongoDBSchema.MongoDBTable; @@ -68,4 +69,9 @@ public boolean couldAffectSchema() { return true; } + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } + } diff --git a/src/sqlancer/mongodb/query/MongoDBCreateTableQuery.java b/src/sqlancer/mongodb/query/MongoDBCreateTableQuery.java index da4381079..7bc174c77 100644 --- a/src/sqlancer/mongodb/query/MongoDBCreateTableQuery.java +++ b/src/sqlancer/mongodb/query/MongoDBCreateTableQuery.java @@ -12,6 +12,7 @@ import sqlancer.GlobalState; import sqlancer.Main; +import sqlancer.common.query.ExpectedErrors; import sqlancer.mongodb.MongoDBConnection; import sqlancer.mongodb.MongoDBQueryAdapter; @@ -44,6 +45,11 @@ public boolean couldAffectSchema() { return true; } + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } + @Override public String getLogString() { String helper = ""; diff --git a/src/sqlancer/mongodb/query/MongoDBInsertQuery.java b/src/sqlancer/mongodb/query/MongoDBInsertQuery.java index 8276a0895..8a41a1341 100644 --- a/src/sqlancer/mongodb/query/MongoDBInsertQuery.java +++ b/src/sqlancer/mongodb/query/MongoDBInsertQuery.java @@ -8,6 +8,7 @@ import com.mongodb.client.result.InsertOneResult; import sqlancer.GlobalState; +import sqlancer.common.query.ExpectedErrors; import sqlancer.mongodb.MongoDBConnection; import sqlancer.mongodb.MongoDBQueryAdapter; import sqlancer.mongodb.MongoDBSchema.MongoDBTable; @@ -76,4 +77,9 @@ public boolean couldAffectSchema() { .insertOne(documentToBeInserted); return result.wasAcknowledged(); } + + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } } diff --git a/src/sqlancer/mongodb/query/MongoDBSelectQuery.java b/src/sqlancer/mongodb/query/MongoDBSelectQuery.java index 91480ed4b..0bcfd4d60 100644 --- a/src/sqlancer/mongodb/query/MongoDBSelectQuery.java +++ b/src/sqlancer/mongodb/query/MongoDBSelectQuery.java @@ -11,6 +11,7 @@ import com.mongodb.client.MongoCursor; import sqlancer.GlobalState; +import sqlancer.common.query.ExpectedErrors; import sqlancer.common.query.SQLancerResultSet; import sqlancer.mongodb.MongoDBConnection; import sqlancer.mongodb.MongoDBQueryAdapter; @@ -39,6 +40,48 @@ public boolean couldAffectSchema() { throw new UnsupportedOperationException(); } + @Override + public ExpectedErrors getExpectedErrors() { + ExpectedErrors errors = new ExpectedErrors(); + // REGEX + errors.add("Regular expression is invalid: nothing to repeat"); + errors.add("Regular expression is invalid: missing terminating ] for character class"); + errors.add("Regular expression is invalid: unmatched parentheses"); + errors.add("Regular expression is invalid: missing )"); + errors.add("Regular expression is invalid: invalid UTF-8 string"); + errors.add("Regular expression is invalid: \\k is not followed by a braced, angle-bracketed, or quoted name"); + errors.add("Regular expression is invalid: missing opening brace after \\\\o"); + errors.add("Regular expression is invalid: reference to non-existent subpattern"); + errors.add("Regular expression is invalid: \\ at end of pattern"); + errors.add("Regular expression is invalid: PCRE does not support \\L, \\l, \\N{name}, \\U, or \\u"); + errors.add("Regular expression is invalid: (?R or (?[+-]digits must be followed by )"); + errors.add("Regular expression is invalid: unknown property name after \\P or \\p"); + errors.add("Regular expression is invalid: (*VERB) not recognized or malformed"); + errors.add("Regular expression is invalid: a numbered reference must not be zero"); + errors.add("Regular expression is invalid: unrecognized character after (? or (?-"); + errors.add("Regular expression is invalid: \\c at end of pattern"); + errors.add("Regular expression is invalid: malformed \\P or \\p sequence"); + errors.add("Regular expression is invalid: range out of order in character class"); + errors.add("Regular expression is invalid: group name must start with a non-digit"); + errors.add("Regular expression is invalid: \\c must be followed by an ASCII character"); + errors.add("Regular expression is invalid: subpattern name expected"); + errors.add("Regular expression is invalid: POSIX collating elements are not supported"); + errors.add("Regular expression is invalid: closing ) for (?C expected"); + errors.add("Regular expression is invalid: syntax error in subpattern name (missing terminator)"); + errors.add("Regular expression is invalid: \\\\N is not supported in a class"); + errors.add("Regular expression is invalid: non-octal character in \\o{} (closing brace missing?)"); + errors.add("Regular expression is invalid: non-hex character in \\x{} (closing brace missing?)"); + errors.add( + "Regular expression is invalid: \\g is not followed by a braced, angle-bracketed, or quoted name/number or by a plain number"); + errors.add("Regular expression is invalid: digits missing in \\x{} or \\o{}"); + errors.add("Regular expression is invalid: malformed number or name after (?("); + errors.add("Regular expression is invalid: digit expected after (?+"); + errors.add("Regular expression is invalid: assertion expected after (?( or (?(?C)"); + errors.add("Regular expression is invalid: unrecognized character after (?P"); + + return errors; + } + @Override public > SQLancerResultSet executeAndGet(G globalState, String... fills) throws Exception { diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java index 8c9522aaa..e0f4716f6 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java @@ -1,5 +1,7 @@ package sqlancer.mongodb.test; +import static sqlancer.mongodb.MongoDBComparatorHelper.getResultSetAsDocumentList; + import java.util.List; import org.bson.Document; @@ -20,17 +22,15 @@ public void check() throws Exception { select.setFilterClause(null); MongoDBSelectQuery q = new MongoDBSelectQuery(select); q.executeAndGet(state); + List firstResultSet = getResultSetAsDocumentList(q, state); - List firstResultSet = q.getResultSet(); select.setFilterClause(predicate); q = new MongoDBSelectQuery(select); - q.executeAndGet(state); - List secondResultSet = q.getResultSet(); + List secondResultSet = getResultSetAsDocumentList(q, state); select.setFilterClause(negatedPredicate); q = new MongoDBSelectQuery(select); - q.executeAndGet(state); - List thirdResultSet = q.getResultSet(); + List thirdResultSet = getResultSetAsDocumentList(q, state); secondResultSet.addAll(thirdResultSet); MongoDBComparatorHelper.assumeResultSetsAreEqual(firstResultSet, secondResultSet, q); diff --git a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java index bef5e5952..5c68fd8bb 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java @@ -60,8 +60,6 @@ public String visit(MongoDBBinaryComparisonNode expr) { public String visit(MongoDBRegexNode expr) { Node left = expr.getLeft(); Node right = expr.getRight(); - assert left instanceof MongoDBColumnTestReference; - assert right instanceof MongoDBConstant.MongoDBStringConstant; return "{\"" + ((MongoDBColumnTestReference) left).getQueryString() + "\": {" + expr.operator().getTextRepresentation() + ": \'" diff --git a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java index fb027add6..be1150d63 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java @@ -97,10 +97,8 @@ public Bson visit(MongoDBRegexNode expr) { Node left = expr.getLeft(); Node right = expr.getRight(); - assert left instanceof MongoDBColumnTestReference; - assert right instanceof MongoDBStringConstant; - String columnName = ((MongoDBColumnTestReference) left).getQueryString(); + return expr.operator().applyOperator(columnName, (MongoDBStringConstant) right); } From bd525f635d7263d79591b63dfb830bcfc613d78d Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 31 Jan 2021 15:17:33 +0100 Subject: [PATCH 08/24] Add expected errors for computed fields in project stage MongoDB Due to the randomly generated computed field tree that has random types as leaf nodes, there are exceptions that we ignore such as wrong type, value not positive for sqrt for example and similar issues. --- .../mongodb/query/MongoDBSelectQuery.java | 29 +++++++++++++++++++ .../MongoDBQueryPartitioningWhereTester.java | 1 - 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/sqlancer/mongodb/query/MongoDBSelectQuery.java b/src/sqlancer/mongodb/query/MongoDBSelectQuery.java index 0bcfd4d60..1288f114c 100644 --- a/src/sqlancer/mongodb/query/MongoDBSelectQuery.java +++ b/src/sqlancer/mongodb/query/MongoDBSelectQuery.java @@ -43,6 +43,35 @@ public boolean couldAffectSchema() { @Override public ExpectedErrors getExpectedErrors() { ExpectedErrors errors = new ExpectedErrors(); + // ARITHMETIC + errors.add("Failed to optimize pipeline :: caused by :: Can't coerce out of range value"); + errors.add("Can't coerce out of range value"); + errors.add("date overflow in $add"); + errors.add("Failed to optimize pipeline :: caused by :: $sqrt only supports numeric types, not"); + errors.add("Failed to optimize pipeline :: caused by :: $sqrt's argument must be greater than or equal to 0"); + errors.add("Failed to optimize pipeline :: caused by :: $pow's base must be numeric, not"); + errors.add("Failed to optimize pipeline :: caused by :: $pow cannot take a base of 0 and a negative exponent"); + errors.add("Failed to optimize pipeline :: caused by :: $add only supports numeric or date types, not"); + errors.add("Failed to optimize pipeline :: caused by :: $exp only supports numeric types, not"); + errors.add("Failed to optimize pipeline :: caused by :: $log's base must be numeric, not"); + errors.add("Failed to optimize pipeline :: caused by :: $log's base must be a positive number not equal to 1"); + errors.add("Failed to optimize pipeline :: caused by :: $multiply only supports numeric types, not"); + errors.add("$log's argument must be numeric, not"); + errors.add("$log's argument must be a positive number, but"); + errors.add("$log's base must be numeric, not"); + errors.add("$log's base must be a positive number not equal to 1"); + errors.add("$divide only supports numeric types, not"); + errors.add("can't $divide by zero"); + errors.add("$pow's exponent must be numeric, not"); + errors.add("$pow's base must be numeric, not"); + errors.add("$pow cannot take a base of 0 and a negative exponent"); + errors.add("$add only supports numeric or date types, not"); + errors.add("only one date allowed in an $add expression"); + errors.add("$multiply only supports numeric types, not"); + errors.add("$exp only supports numeric types, not"); + errors.add("$sqrt's argument must be greater than or equal to 0"); + errors.add("$sqrt only supports numeric types, not"); + // REGEX errors.add("Regular expression is invalid: nothing to repeat"); errors.add("Regular expression is invalid: missing terminating ] for character class"); diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java index e0f4716f6..a9695d1d2 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java @@ -21,7 +21,6 @@ public void check() throws Exception { select.setFilterClause(null); MongoDBSelectQuery q = new MongoDBSelectQuery(select); - q.executeAndGet(state); List firstResultSet = getResultSetAsDocumentList(q, state); select.setFilterClause(predicate); From 6aa53858cb8d89b0a7de701335a28d9d2d51a76c Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 31 Jan 2021 16:15:14 +0100 Subject: [PATCH 09/24] Add logging of computed fields in MongoDB --- .../mongodb/visitor/MongoDBToLogVisitor.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java index 5c68fd8bb..76b1241a4 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; +import sqlancer.common.ast.newast.NewFunctionNode; import sqlancer.common.ast.newast.Node; import sqlancer.mongodb.ast.MongoDBBinaryComparisonNode; import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; @@ -11,6 +12,7 @@ import sqlancer.mongodb.ast.MongoDBRegexNode; import sqlancer.mongodb.ast.MongoDBSelect; import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; +import sqlancer.mongodb.gen.MongoDBComputedExpressionGenerator.ComputedFunction; import sqlancer.mongodb.test.MongoDBColumnTestReference; public class MongoDBToLogVisitor extends MongoDBVisitor { @@ -35,6 +37,49 @@ public String visitLog(Node expr) { } } + public String visitComputed(Node expr) { + if (expr instanceof NewFunctionNode) { + return visitComputed((NewFunctionNode) expr); + } else { + throw new AssertionError(expr.getClass()); + } + } + + public String visitComputed(NewFunctionNode expr) { + List arguments = new ArrayList<>(); + for (int i = 0; i < expr.getArgs().size(); i++) { + if (expr.getArgs().get(i) instanceof MongoDBConstant) { + arguments.add(((MongoDBConstant) expr.getArgs().get(i)).getLogValue()); + continue; + } + if (expr.getArgs().get(i) instanceof MongoDBColumnTestReference) { + arguments.add("\"$" + ((MongoDBColumnTestReference) expr.getArgs().get(i)).getQueryString() + "\""); + continue; + } + if (expr.getArgs().get(i) instanceof NewFunctionNode) { + arguments.add(visitComputed((NewFunctionNode) expr.getArgs().get(i))); + } else { + throw new AssertionError(); + } + } + if (!(expr.getFunc() instanceof ComputedFunction)) { + throw new AssertionError(expr.getClass()); + } + + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append(((ComputedFunction) expr.getFunc()).getOperator()); + sb.append(": ["); + String helper = ""; + for (String arg : arguments) { + sb.append(helper); + helper = ", "; + sb.append(arg); + } + sb.append("]}"); + return sb.toString(); + } + public String visit(MongoDBUnaryLogicalOperatorNode expr) { String inner = visitLog(expr.getExpr()); return expr.operator().getTextRepresentation() + inner + "]}"; @@ -108,6 +153,17 @@ private void setProjects(MongoDBSelect select) { helper = ","; sb.append("\"").append(reference.getQueryString()).append("\"").append(": 1"); } + sb.append("\n"); + if (select.hasComputed()) { + String name = "computed"; + int number = 0; + for (Node expressionNode : select.getComputedClause()) { + sb.append(helper); + helper = ",\n"; + sb.append("\"" + name + number + "\": " + visitComputed(expressionNode)); + number++; + } + } sb.append("}"); projects = sb.toString(); } From 6020a07d7491fdbc0cef16a4de08ca1c35bbbee0 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 31 Jan 2021 17:44:34 +0100 Subject: [PATCH 10/24] Add randomized regex options for the regular expression node in MongoDB The options are flags that can be set in a string such as "im". The expression generator now generates a random valid options for the regular expression operator in MongoDB. --- src/sqlancer/mongodb/ast/MongoDBRegexNode.java | 9 ++++++++- .../gen/MongoDBMatchExpressionGenerator.java | 15 +++++++++++---- .../mongodb/visitor/MongoDBToLogVisitor.java | 3 ++- .../mongodb/visitor/MongoDBToQueryVisitor.java | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/sqlancer/mongodb/ast/MongoDBRegexNode.java b/src/sqlancer/mongodb/ast/MongoDBRegexNode.java index 154ebeffa..76c608586 100644 --- a/src/sqlancer/mongodb/ast/MongoDBRegexNode.java +++ b/src/sqlancer/mongodb/ast/MongoDBRegexNode.java @@ -7,8 +7,15 @@ import sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBRegexOperator; public class MongoDBRegexNode extends NewBinaryOperatorNode { - public MongoDBRegexNode(Node left, Node right) { + private final String options; + + public MongoDBRegexNode(Node left, Node right, String options) { super(left, right, REGEX); + this.options = options; + } + + public String getOptions() { + return options; } public MongoDBRegexOperator operator() { diff --git a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java index e11de1165..e02064f5c 100644 --- a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java @@ -53,7 +53,8 @@ public Node generateLeafNode() { return new MongoDBBinaryComparisonNode(generateColumn(), generateConstant(), operator); case REGEX: return new MongoDBRegexNode(generateColumn(), - new MongoDBConstantGenerator(globalState).generateConstantWithType(MongoDBDataType.STRING)); + new MongoDBConstantGenerator(globalState).generateConstantWithType(MongoDBDataType.STRING), + getRandomizedRegexOptions()); default: throw new AssertionError(); } @@ -90,6 +91,11 @@ public Node generateConstant() { return generator.generateConstantWithType(type); } + private String getRandomizedRegexOptions() { + List s = Randomly.subset("i", "m", "x", "s"); + return s.stream().reduce("", (current, newVal) -> current + newVal); + } + @Override protected Node generateColumn() { return Randomly.fromList(columns); @@ -249,8 +255,8 @@ public static MongoDBBinaryComparisonOperator getRandom() { public enum MongoDBRegexOperator implements Operator { REGEX { @Override - public Bson applyOperator(String columnName, MongoDBConstant.MongoDBStringConstant regex) { - return Filters.regex(columnName, regex.getStringValue(), ""); + public Bson applyOperator(String columnName, MongoDBConstant.MongoDBStringConstant regex, String options) { + return Filters.regex(columnName, regex.getStringValue(), options); } @Override @@ -259,6 +265,7 @@ public String getTextRepresentation() { } }; - public abstract Bson applyOperator(String columnName, MongoDBConstant.MongoDBStringConstant regex); + public abstract Bson applyOperator(String columnName, MongoDBConstant.MongoDBStringConstant regex, + String options); } } diff --git a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java index 76b1241a4..37465163b 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java @@ -108,7 +108,8 @@ public String visit(MongoDBRegexNode expr) { return "{\"" + ((MongoDBColumnTestReference) left).getQueryString() + "\": {" + expr.operator().getTextRepresentation() + ": \'" - + ((MongoDBConstant.MongoDBStringConstant) right).getStringValue() + "\', $options: \'\'}}"; + + ((MongoDBConstant.MongoDBStringConstant) right).getStringValue() + "\', $options: \'" + + expr.getOptions() + "\'}}"; } @Override diff --git a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java index be1150d63..dcb8718ce 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java @@ -99,7 +99,7 @@ public Bson visit(MongoDBRegexNode expr) { String columnName = ((MongoDBColumnTestReference) left).getQueryString(); - return expr.operator().applyOperator(columnName, (MongoDBStringConstant) right); + return expr.operator().applyOperator(columnName, (MongoDBStringConstant) right, expr.getOptions()); } public Bson visit(MongoDBBinaryComparisonNode expr) { From 5897c8673e77454538833aaa8b3bf86e533c118d Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 1 Feb 2021 10:25:04 +0100 Subject: [PATCH 11/24] Add a variation where the count is compared to the number of returned documents in MongoDB This new variation introduces a variant where we execute the pipeline query once with a count as a last pipeline stage and compare the output to the result set without the count stage. --- .../mongodb/MongoDBComparatorHelper.java | 24 +++++++++++++++++++ src/sqlancer/mongodb/MongoDBOptions.java | 3 +++ src/sqlancer/mongodb/ast/MongoDBSelect.java | 9 +++++++ .../MongoDBQueryPartitioningWhereTester.java | 10 ++++++++ .../mongodb/visitor/MongoDBToLogVisitor.java | 9 ++++++- .../visitor/MongoDBToQueryVisitor.java | 13 ++++++++++ 6 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/sqlancer/mongodb/MongoDBComparatorHelper.java b/src/sqlancer/mongodb/MongoDBComparatorHelper.java index 02996cfda..c14f8cdbf 100644 --- a/src/sqlancer/mongodb/MongoDBComparatorHelper.java +++ b/src/sqlancer/mongodb/MongoDBComparatorHelper.java @@ -41,6 +41,30 @@ public static List getResultSetAsDocumentList(MongoDBSelectQuery adapt } } + public static void assumeCountIsEqual(List resultSet, List secondResultSet, + MongoDBSelectQuery originalQuery) { + int originalSize = resultSet.size(); + if (secondResultSet.isEmpty()) { + if (originalSize == 0) { + return; + } else { + String assertMessage = String.format("The Count of the result set mismatches!\n %s", + originalQuery.getLogString()); + throw new AssertionError(assertMessage); + } + } + if (secondResultSet.size() != 1) { + throw new AssertionError( + String.format("Count query result bigger than one \n %s", originalQuery.getLogString())); + } + int withCount = (int) secondResultSet.get(0).get("count"); + if (originalSize != withCount) { + String assertMessage = String.format("The Count of the result set mismatches!\n %s", + originalQuery.getLogString()); + throw new AssertionError(assertMessage); + } + } + public static void assumeResultSetsAreEqual(List resultSet, List secondResultSet, MongoDBSelectQuery originalQuery) { if (resultSet.size() != secondResultSet.size()) { diff --git a/src/sqlancer/mongodb/MongoDBOptions.java b/src/sqlancer/mongodb/MongoDBOptions.java index 5c16bfd4d..5023a6fb8 100644 --- a/src/sqlancer/mongodb/MongoDBOptions.java +++ b/src/sqlancer/mongodb/MongoDBOptions.java @@ -34,6 +34,9 @@ public class MongoDBOptions implements DBMSSpecificOptions oracles = Arrays.asList(QUERY_PARTITIONING); diff --git a/src/sqlancer/mongodb/ast/MongoDBSelect.java b/src/sqlancer/mongodb/ast/MongoDBSelect.java index de65fb01d..0fe91ba4a 100644 --- a/src/sqlancer/mongodb/ast/MongoDBSelect.java +++ b/src/sqlancer/mongodb/ast/MongoDBSelect.java @@ -15,6 +15,7 @@ public class MongoDBSelect implements Node { Node filterClause; boolean hasComputed; List> computedClauses; + private boolean withCountClause; public MongoDBSelect(String mainTableName, MongoDBColumnTestReference joinColumn) { this.mainTableName = mainTableName; @@ -92,4 +93,12 @@ public List> getComputedClause() { public boolean hasComputed() { return hasComputed; } + + public boolean getWithCountClause() { + return withCountClause; + } + + public void setWithCountClause(boolean withCountClause) { + this.withCountClause = withCountClause; + } } diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java index a9695d1d2..5a7507672 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningWhereTester.java @@ -19,6 +19,8 @@ public MongoDBQueryPartitioningWhereTester(MongoDBGlobalState state) { public void check() throws Exception { super.check(); + select.setWithCountClause(false); + select.setFilterClause(null); MongoDBSelectQuery q = new MongoDBSelectQuery(select); List firstResultSet = getResultSetAsDocumentList(q, state); @@ -31,6 +33,14 @@ public void check() throws Exception { q = new MongoDBSelectQuery(select); List thirdResultSet = getResultSetAsDocumentList(q, state); + if (state.getDmbsSpecificOptions().testWithCount) { + select.setWithCountClause(true); + select.setFilterClause(predicate); + q = new MongoDBSelectQuery(select); + List forthResultSet = getResultSetAsDocumentList(q, state); + MongoDBComparatorHelper.assumeCountIsEqual(secondResultSet, forthResultSet, q); + } + secondResultSet.addAll(thirdResultSet); MongoDBComparatorHelper.assumeResultSetsAreEqual(firstResultSet, secondResultSet, q); diff --git a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java index 37465163b..1fbae32a6 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java @@ -22,6 +22,7 @@ public class MongoDBToLogVisitor extends MongoDBVisitor { private String filter; private String projects; private boolean hasFilter; + private boolean withCount; public String visitLog(Node expr) { if (expr instanceof MongoDBUnaryLogicalOperatorNode) { @@ -126,6 +127,7 @@ public void visit(MongoDBSelect select) { setFilter(select); } setProjects(select); + withCount = select.getWithCountClause(); } private void setFilter(MongoDBSelect select) { @@ -182,7 +184,12 @@ public String getStringLog() { } sb.append("{ $project : "); sb.append(projects); - sb.append("}])\n"); + sb.append("}"); + if (withCount) { + sb.append(",\n"); + sb.append(" {$count: \"count\"}\n"); + } + sb.append("])\n"); return sb.toString(); } } diff --git a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java index dcb8718ce..8efbf97e4 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToQueryVisitor.java @@ -33,7 +33,9 @@ public class MongoDBToQueryVisitor extends MongoDBVisitor { private List lookup; private Bson filter; private Bson projection; + private Bson count; private boolean hasFilter; + private boolean hasCountClause; public Bson visitBson(Node expr) { if (expr instanceof MongoDBUnaryLogicalOperatorNode) { @@ -125,6 +127,14 @@ public void visit(MongoDBSelect select) { setFilter(select); } setProjection(select); + hasCountClause = select.getWithCountClause(); + if (hasCountClause) { + setCount(); + } + } + + private void setCount() { + count = Aggregates.count("count"); } private void setFilter(MongoDBSelect select) { @@ -166,6 +176,9 @@ public List getPipeline() { result.add(filter); } result.add(projection); + if (hasCountClause) { + result.add(count); + } return result; } } From 85d1415a62adbf86ad2f7af8ff5ee913c42f3602 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 2 Feb 2021 23:30:15 +0100 Subject: [PATCH 12/24] Rework negation of MongoDB expression to better cover the api Until now not has been simulated by nor(id exists, bool_expression) and it has led to believe that the underlying structure works fine. Now after the rework the not gets evaluated and every logical operation inverted by the new NegateVisitor. At the lowest stage type problems remain. --- .../mongodb/MongoDBComparatorHelper.java | 2 +- .../gen/MongoDBMatchExpressionGenerator.java | 14 +- .../mongodb/visitor/MongoDBNegateVisitor.java | 150 ++++++++++++++++++ .../mongodb/visitor/MongoDBToLogVisitor.java | 2 +- .../mongodb/visitor/MongoDBVisitor.java | 6 + 5 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 src/sqlancer/mongodb/visitor/MongoDBNegateVisitor.java diff --git a/src/sqlancer/mongodb/MongoDBComparatorHelper.java b/src/sqlancer/mongodb/MongoDBComparatorHelper.java index c14f8cdbf..8554a6662 100644 --- a/src/sqlancer/mongodb/MongoDBComparatorHelper.java +++ b/src/sqlancer/mongodb/MongoDBComparatorHelper.java @@ -69,7 +69,7 @@ public static void assumeResultSetsAreEqual(List resultSet, List, MongoDBColumnTestReference> { @@ -101,9 +102,17 @@ protected Node generateColumn() { return Randomly.fromList(columns); } + @Override + public Node generatePredicate() { + Node result = super.generatePredicate(); + return MongoDBNegateVisitor.cleanNegations(result); + } + @Override public Node negatePredicate(Node predicate) { - return new MongoDBUnaryLogicalOperatorNode(predicate, MongoDBUnaryLogicalOperator.NOT); + Node result = new MongoDBUnaryLogicalOperatorNode(predicate, + MongoDBUnaryLogicalOperator.NOT); + return MongoDBNegateVisitor.cleanNegations(result); } @Override @@ -115,12 +124,13 @@ public enum MongoDBUnaryLogicalOperator implements Operator { NOT { @Override public Bson applyOperator(Bson inner) { + // return Filters.not(inner); TODO: Patrick return Filters.nor(inner, Filters.exists("_id", false)); } @Override public String getTextRepresentation() { - return "{$nor: [{ _id: {$exists: false}}, "; + return "$not"; } }; diff --git a/src/sqlancer/mongodb/visitor/MongoDBNegateVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBNegateVisitor.java new file mode 100644 index 000000000..23192c2b0 --- /dev/null +++ b/src/sqlancer/mongodb/visitor/MongoDBNegateVisitor.java @@ -0,0 +1,150 @@ +package sqlancer.mongodb.visitor; + +import static sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBBinaryLogicalOperator.AND; +import static sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBBinaryLogicalOperator.NOR; +import static sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBBinaryLogicalOperator.OR; +import static sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator.MongoDBUnaryLogicalOperator.NOT; + +import sqlancer.common.ast.newast.Node; +import sqlancer.mongodb.ast.MongoDBBinaryComparisonNode; +import sqlancer.mongodb.ast.MongoDBBinaryLogicalNode; +import sqlancer.mongodb.ast.MongoDBConstant; +import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBRegexNode; +import sqlancer.mongodb.ast.MongoDBSelect; +import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; + +public class MongoDBNegateVisitor extends MongoDBVisitor { + + private boolean negate; + Node negatedExpression; + + public MongoDBNegateVisitor(boolean negate) { + this.negate = negate; + } + + @Override + public void visit(Node expr) { + if (expr instanceof MongoDBConstant) { + visit((MongoDBConstant) expr); + } else if (expr instanceof MongoDBSelect) { + visit((MongoDBSelect) expr); + } else if (expr instanceof MongoDBBinaryComparisonNode) { + visit((MongoDBBinaryComparisonNode) expr); + } else if (expr instanceof MongoDBUnaryLogicalOperatorNode) { + visit((MongoDBUnaryLogicalOperatorNode) expr); + } else if (expr instanceof MongoDBRegexNode) { + visit((MongoDBRegexNode) expr); + } else if (expr instanceof MongoDBBinaryLogicalNode) { + visit((MongoDBBinaryLogicalNode) expr); + } else { + throw new AssertionError(expr.getClass()); + } + } + + public void visit(MongoDBBinaryComparisonNode expr) { + + if (negate) { + negatedExpression = new MongoDBUnaryLogicalOperatorNode(expr, NOT); + // TODO: Patrick + /* + * switch (expr.operator()) { case EQUALS: negatedExpression = new + * MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.NOT_EQUALS); break; case NOT_EQUALS: + * negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.EQUALS); break; + * + * case LESS: negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.GREATER_EQUAL); break; case LESS_EQUAL: + * negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.GREATER); break; case GREATER: + * negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.LESS_EQUAL); break; case GREATER_EQUAL: + * negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.LESS); break; default: throw new + * UnsupportedOperationException(); } + */ + } else { + negatedExpression = expr; + } + } + + public void visit(MongoDBRegexNode expr) { + if (negate) { + negatedExpression = new MongoDBUnaryLogicalOperatorNode(expr, NOT); + } else { + negatedExpression = expr; + } + } + + public void visit(MongoDBUnaryLogicalOperatorNode expr) { + if (!(expr.operator().equals(NOT))) { + throw new UnsupportedOperationException(); + } + negate = !negate; + visit(expr.getExpr()); + } + + public void visit(MongoDBBinaryLogicalNode expr) { + boolean saveNegate = negate; + Node left; + Node right; + switch (expr.operator()) { + case OR: + negate = false; + visit(expr.getLeft()); + left = negatedExpression; + negate = false; + visit(expr.getRight()); + right = negatedExpression; + if (saveNegate) { + negatedExpression = new MongoDBBinaryLogicalNode(left, right, NOR); + } else { + negatedExpression = new MongoDBBinaryLogicalNode(left, right, OR); + } + break; + case AND: + negate = saveNegate; + visit(expr.getLeft()); + left = negatedExpression; + negate = saveNegate; + visit(expr.getRight()); + right = negatedExpression; + if (saveNegate) { + negatedExpression = new MongoDBBinaryLogicalNode(left, right, OR); + } else { + negatedExpression = new MongoDBBinaryLogicalNode(left, right, AND); + } + break; + case NOR: + negate = false; + visit(expr.getLeft()); + left = negatedExpression; + negate = false; + visit(expr.getRight()); + right = negatedExpression; + if (saveNegate) { + negatedExpression = new MongoDBBinaryLogicalNode(left, right, OR); + } else { + negatedExpression = new MongoDBBinaryLogicalNode(left, right, NOR); + } + break; + default: + throw new UnsupportedOperationException(expr.getOperatorRepresentation()); + } + } + + @Override + public void visit(MongoDBConstant c) { + negatedExpression = c; + } + + @Override + public void visit(MongoDBSelect s) { + throw new UnsupportedOperationException(); + } + + public Node getNegatedExpression() { + return negatedExpression; + } +} diff --git a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java index 1fbae32a6..4c55e17a6 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBToLogVisitor.java @@ -83,7 +83,7 @@ public String visitComputed(NewFunctionNode expr) { public String visit(MongoDBUnaryLogicalOperatorNode expr) { String inner = visitLog(expr.getExpr()); - return expr.operator().getTextRepresentation() + inner + "]}"; + return "{ " + expr.operator().getTextRepresentation() + ": [" + inner + "]}"; } public String visit(MongoDBBinaryLogicalNode expr) { diff --git a/src/sqlancer/mongodb/visitor/MongoDBVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBVisitor.java index 67b6ca741..e02a50f02 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBVisitor.java @@ -36,4 +36,10 @@ public static String asStringLog(Node expr) { visitor.visit(expr); return visitor.getStringLog(); } + + public static Node cleanNegations(Node expr) { + MongoDBNegateVisitor visitor = new MongoDBNegateVisitor(false); + visitor.visit(expr); + return visitor.getNegatedExpression(); + } } From 498b9de9a8b1ecb1dbcbb4c72916b964b0f7fd8f Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 3 Feb 2021 00:03:46 +0100 Subject: [PATCH 13/24] Fix MongoDB unsuccessful and successful query counts --- src/sqlancer/mongodb/MongoDBComparatorHelper.java | 6 +++--- src/sqlancer/mongodb/query/MongoDBInsertQuery.java | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sqlancer/mongodb/MongoDBComparatorHelper.java b/src/sqlancer/mongodb/MongoDBComparatorHelper.java index 8554a6662..b03e24422 100644 --- a/src/sqlancer/mongodb/MongoDBComparatorHelper.java +++ b/src/sqlancer/mongodb/MongoDBComparatorHelper.java @@ -7,6 +7,7 @@ import org.bson.Document; import sqlancer.IgnoreMeException; +import sqlancer.Main; import sqlancer.common.query.ExpectedErrors; import sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState; import sqlancer.mongodb.query.MongoDBSelectQuery; @@ -22,18 +23,17 @@ public static List getResultSetAsDocumentList(MongoDBSelectQuery adapt List result; try { adapter.executeAndGet(state); + Main.nrSuccessfulActions.addAndGet(1); result = adapter.getResultSet(); return result; - } catch (Exception e) { + Main.nrUnsuccessfulActions.addAndGet(1); if (e instanceof IgnoreMeException) { throw e; } - if (e.getMessage() == null) { throw new AssertionError(adapter.getLogString(), e); } - if (errors.errorIsExpected(e.getMessage())) { throw new IgnoreMeException(); } diff --git a/src/sqlancer/mongodb/query/MongoDBInsertQuery.java b/src/sqlancer/mongodb/query/MongoDBInsertQuery.java index 8a41a1341..127dc82d0 100644 --- a/src/sqlancer/mongodb/query/MongoDBInsertQuery.java +++ b/src/sqlancer/mongodb/query/MongoDBInsertQuery.java @@ -8,6 +8,7 @@ import com.mongodb.client.result.InsertOneResult; import sqlancer.GlobalState; +import sqlancer.Main; import sqlancer.common.query.ExpectedErrors; import sqlancer.mongodb.MongoDBConnection; import sqlancer.mongodb.MongoDBQueryAdapter; @@ -73,6 +74,7 @@ public boolean couldAffectSchema() { @Override public > boolean execute(G globalState, String... fills) throws Exception { + Main.nrSuccessfulActions.addAndGet(1); InsertOneResult result = globalState.getConnection().getDatabase().getCollection(table.getName()) .insertOne(documentToBeInserted); return result.wasAcknowledged(); From 78a67d152f101be16bc74de455cbf0e39d9680cf Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 6 Feb 2021 17:14:40 +0100 Subject: [PATCH 14/24] Add a new way to negate a query that replaces the old placeholder This change reveals some issues with how we form queries for mongodb. The core of it seems to be that neither greater equal nor smaller include null values and it is rather hard to define a query that finds the ones that are null. The query that projects includes all. --- src/sqlancer/mongodb/MongoDBSchema.java | 14 +++++- .../mongodb/gen/MongoDBConstantGenerator.java | 11 +++-- .../gen/MongoDBMatchExpressionGenerator.java | 16 +++++-- .../test/MongoDBQueryPartitioningBase.java | 23 ++++----- .../mongodb/visitor/MongoDBNegateVisitor.java | 47 ++++++++++++------- 5 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/sqlancer/mongodb/MongoDBSchema.java b/src/sqlancer/mongodb/MongoDBSchema.java index 5bf3cf2b5..535e0d5f8 100644 --- a/src/sqlancer/mongodb/MongoDBSchema.java +++ b/src/sqlancer/mongodb/MongoDBSchema.java @@ -56,7 +56,19 @@ public BsonType getBsonType() { }; public static MongoDBDataType getRandom() { - return Randomly.fromOptions(values()); + // TODO: If String is enabled, there are type issues. Find a way to have a cast or operation on top of the + // query + // TODO: to solve this issue. + MongoDBDataType[] valuesWithoutString = new MongoDBDataType[values().length - 1]; + int i = 0; + for (MongoDBDataType type : values()) { + if (type.equals(STRING)) { + continue; + } + valuesWithoutString[i] = type; + i++; + } + return Randomly.fromOptions(valuesWithoutString); } } diff --git a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java index c0a8bdb77..bda5c87e6 100644 --- a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java @@ -12,7 +12,6 @@ import sqlancer.mongodb.ast.MongoDBConstant.MongoDBDoubleConstant; import sqlancer.mongodb.ast.MongoDBConstant.MongoDBIntegerConstant; import sqlancer.mongodb.ast.MongoDBConstant.MongoDBNullConstant; -import sqlancer.mongodb.ast.MongoDBConstant.MongoDBStringConstant; import sqlancer.mongodb.ast.MongoDBConstant.MongoDBTimestampConstant; import sqlancer.mongodb.ast.MongoDBExpression; @@ -68,10 +67,12 @@ public void addRandomConstantWithType(Document document, String key, MongoDBData constant = new MongoDBDoubleConstant(globalState.getRandomly().getDouble()); constant.setValueInDocument(document, key); return; - case STRING: - constant = new MongoDBStringConstant(globalState.getRandomly().getString()); - constant.setValueInDocument(document, key); - return; + // TODO: If String is enabled, there are type issues. Find a way to have a cast or operation on top of the query + // TODO: to solve this issue. + // case STRING: + // constant = new MongoDBStringConstant(globalState.getRandomly().getString()); + // constant.setValueInDocument(document, key); + // return; case INTEGER: constant = new MongoDBIntegerConstant((int) globalState.getRandomly().getInteger()); constant.setValueInDocument(document, key); diff --git a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java index c298295c7..6414ea185 100644 --- a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java @@ -51,7 +51,10 @@ public Node generateLeafNode() { switch (expr) { case BINARY_COMPARISON: MongoDBBinaryComparisonOperator operator = MongoDBBinaryComparisonOperator.getRandom(); - return new MongoDBBinaryComparisonNode(generateColumn(), generateConstant(), operator); + MongoDBColumnTestReference reference = (MongoDBColumnTestReference) generateColumn(); + + return new MongoDBBinaryComparisonNode(reference, + generateConstant(reference.getColumnReference().getType()), operator); case REGEX: return new MongoDBRegexNode(generateColumn(), new MongoDBConstantGenerator(globalState).generateConstantWithType(MongoDBDataType.STRING), @@ -92,6 +95,14 @@ public Node generateConstant() { return generator.generateConstantWithType(type); } + public Node generateConstant(MongoDBDataType type) { + MongoDBConstantGenerator generator = new MongoDBConstantGenerator(globalState); + // if (Randomly.getBooleanWithSmallProbability()) { + // return MongoDBConstant.createNullConstant(); + // } + return generator.generateConstantWithType(type); + } + private String getRandomizedRegexOptions() { List s = Randomly.subset("i", "m", "x", "s"); return s.stream().reduce("", (current, newVal) -> current + newVal); @@ -124,8 +135,7 @@ public enum MongoDBUnaryLogicalOperator implements Operator { NOT { @Override public Bson applyOperator(Bson inner) { - // return Filters.not(inner); TODO: Patrick - return Filters.nor(inner, Filters.exists("_id", false)); + return Filters.not(inner); } @Override diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java index c33bcf9e6..6421482a0 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java @@ -42,11 +42,12 @@ public void check() throws Exception { initializeTernaryPredicateVariants(); select = new MongoDBSelect<>(mainTable.getName(), targetColumns.get(0)); select.setProjectionList(targetColumns); - if (Randomly.getBooleanWithRatherLowProbability()) { - select.setLookupList(targetColumns); - } else { - select.setLookupList(Randomly.nonEmptySubset(targetColumns)); - } + // TODO: Find a new way to split the query such that it can handle null fields to activate lookups + // if (Randomly.getBooleanWithRatherLowProbability()) { + select.setLookupList(targetColumns); + /* + * } else { select.setLookupList(Randomly.nonEmptySubset(targetColumns)); } + */ if (state.getDmbsSpecificOptions().testComputedValues) { generateComputedColumns(); } @@ -69,12 +70,12 @@ private void generateTargetColumns() { targetColumns.add(new MongoDBColumnTestReference(c, true)); } List joinsOtherTables = new ArrayList<>(); - for (int i = 1; i < targetTables.getTables().size(); i++) { - MongoDBTable procTable = targetTables.getTables().get(i); - for (MongoDBColumn c : procTable.getColumns()) { - joinsOtherTables.add(new MongoDBColumnTestReference(c, false)); - } - } + // TODO: Find a new way to split the query such that it can handle null fields to activate lookups + /* + * for (int i = 1; i < targetTables.getTables().size(); i++) { MongoDBTable procTable = + * targetTables.getTables().get(i); for (MongoDBColumn c : procTable.getColumns()) { joinsOtherTables.add(new + * MongoDBColumnTestReference(c, false)); } } + */ if (!joinsOtherTables.isEmpty()) { int randNumber = state.getRandomly().getInteger(1, Math.min(joinsOtherTables.size(), 4)); List subsetJoinsOtherTables = Randomly.nonEmptySubset(joinsOtherTables, diff --git a/src/sqlancer/mongodb/visitor/MongoDBNegateVisitor.java b/src/sqlancer/mongodb/visitor/MongoDBNegateVisitor.java index 23192c2b0..39b607f67 100644 --- a/src/sqlancer/mongodb/visitor/MongoDBNegateVisitor.java +++ b/src/sqlancer/mongodb/visitor/MongoDBNegateVisitor.java @@ -13,6 +13,7 @@ import sqlancer.mongodb.ast.MongoDBRegexNode; import sqlancer.mongodb.ast.MongoDBSelect; import sqlancer.mongodb.ast.MongoDBUnaryLogicalOperatorNode; +import sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator; public class MongoDBNegateVisitor extends MongoDBVisitor { @@ -46,24 +47,34 @@ public void visit(MongoDBBinaryComparisonNode expr) { if (negate) { negatedExpression = new MongoDBUnaryLogicalOperatorNode(expr, NOT); - // TODO: Patrick - /* - * switch (expr.operator()) { case EQUALS: negatedExpression = new - * MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), - * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.NOT_EQUALS); break; case NOT_EQUALS: - * negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), - * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.EQUALS); break; - * - * case LESS: negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), - * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.GREATER_EQUAL); break; case LESS_EQUAL: - * negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), - * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.GREATER); break; case GREATER: - * negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), - * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.LESS_EQUAL); break; case GREATER_EQUAL: - * negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), - * MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.LESS); break; default: throw new - * UnsupportedOperationException(); } - */ + switch (expr.operator()) { + case EQUALS: + negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.NOT_EQUALS); + break; + case NOT_EQUALS: + negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.EQUALS); + break; + case LESS: + negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.GREATER_EQUAL); + break; + case LESS_EQUAL: + negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.GREATER); + break; + case GREATER: + negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.LESS_EQUAL); + break; + case GREATER_EQUAL: + negatedExpression = new MongoDBBinaryComparisonNode(expr.getLeft(), expr.getRight(), + MongoDBMatchExpressionGenerator.MongoDBBinaryComparisonOperator.LESS); + break; + default: + throw new UnsupportedOperationException(); + } } else { negatedExpression = expr; } From dfae8afeb11f83d3c75b6b184074693115e530f4 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 28 Feb 2021 15:12:08 +0100 Subject: [PATCH 15/24] Introduce ArangoDB to SQLancer and support creating collection and inserting data For four data types of ArangoDB integer, double, string and boolean, this commit supports creating collection, keeping track of the schema, creating tables, collections and inserting randomized data. --- pom.xml | 5 + src/sqlancer/Main.java | 2 + src/sqlancer/arangodb/ArangoDBConnection.java | 31 +++++ .../arangodb/ArangoDBLoggableFactory.java | 40 ++++++ src/sqlancer/arangodb/ArangoDBOptions.java | 41 ++++++ src/sqlancer/arangodb/ArangoDBProvider.java | 128 ++++++++++++++++++ .../arangodb/ArangoDBQueryAdapter.java | 15 ++ .../arangodb/ArangoDBQueryProvider.java | 6 + src/sqlancer/arangodb/ArangoDBSchema.java | 66 +++++++++ .../arangodb/ast/ArangoDBConstant.java | 126 +++++++++++++++++ .../arangodb/ast/ArangoDBExpression.java | 4 + .../arangodb/gen/ArangoDBInsertGenerator.java | 39 ++++++ .../arangodb/gen/ArangoDBTableGenerator.java | 44 ++++++ .../query/ArangoDBConstantGenerator.java | 46 +++++++ .../query/ArangoDBCreateTableQuery.java | 45 ++++++ .../arangodb/query/ArangoDBInsertQuery.java | 50 +++++++ .../test/ArangoDBQueryPartitioningBase.java | 27 ++++ .../ArangoDBQueryPartitioningWhereTester.java | 9 ++ 18 files changed, 724 insertions(+) create mode 100644 src/sqlancer/arangodb/ArangoDBConnection.java create mode 100644 src/sqlancer/arangodb/ArangoDBLoggableFactory.java create mode 100644 src/sqlancer/arangodb/ArangoDBOptions.java create mode 100644 src/sqlancer/arangodb/ArangoDBProvider.java create mode 100644 src/sqlancer/arangodb/ArangoDBQueryAdapter.java create mode 100644 src/sqlancer/arangodb/ArangoDBQueryProvider.java create mode 100644 src/sqlancer/arangodb/ArangoDBSchema.java create mode 100644 src/sqlancer/arangodb/ast/ArangoDBConstant.java create mode 100644 src/sqlancer/arangodb/ast/ArangoDBExpression.java create mode 100644 src/sqlancer/arangodb/gen/ArangoDBInsertGenerator.java create mode 100644 src/sqlancer/arangodb/gen/ArangoDBTableGenerator.java create mode 100644 src/sqlancer/arangodb/query/ArangoDBConstantGenerator.java create mode 100644 src/sqlancer/arangodb/query/ArangoDBCreateTableQuery.java create mode 100644 src/sqlancer/arangodb/query/ArangoDBInsertQuery.java create mode 100644 src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java create mode 100644 src/sqlancer/arangodb/test/ArangoDBQueryPartitioningWhereTester.java diff --git a/pom.xml b/pom.xml index c0f0bbd9b..2b8783fb8 100644 --- a/pom.xml +++ b/pom.xml @@ -271,6 +271,11 @@ mongodb-driver-sync 4.1.1 + + com.arangodb + arangodb-java-driver + 6.9.0 + diff --git a/src/sqlancer/Main.java b/src/sqlancer/Main.java index d606ee0f5..bfc0cabd8 100644 --- a/src/sqlancer/Main.java +++ b/src/sqlancer/Main.java @@ -21,6 +21,7 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.JCommander.Builder; +import sqlancer.arangodb.ArangoDBProvider; import sqlancer.citus.CitusProvider; import sqlancer.clickhouse.ClickHouseProvider; import sqlancer.cockroachdb.CockroachDBProvider; @@ -556,6 +557,7 @@ private boolean run(MainOptions options, ExecutorService execService, providers.add(new DuckDBProvider()); providers.add(new H2Provider()); providers.add(new MongoDBProvider()); + providers.add(new ArangoDBProvider()); return providers; } diff --git a/src/sqlancer/arangodb/ArangoDBConnection.java b/src/sqlancer/arangodb/ArangoDBConnection.java new file mode 100644 index 000000000..b3e5b85d3 --- /dev/null +++ b/src/sqlancer/arangodb/ArangoDBConnection.java @@ -0,0 +1,31 @@ +package sqlancer.arangodb; + +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDatabase; + +import sqlancer.SQLancerDBConnection; + +public class ArangoDBConnection implements SQLancerDBConnection { + + private final ArangoDB client; + private final ArangoDatabase database; + + public ArangoDBConnection(ArangoDB client, ArangoDatabase database) { + this.client = client; + this.database = database; + } + + @Override + public String getDatabaseVersion() throws Exception { + return client.getVersion().getVersion(); + } + + @Override + public void close() throws Exception { + client.shutdown(); + } + + public ArangoDatabase getDatabase() { + return database; + } +} diff --git a/src/sqlancer/arangodb/ArangoDBLoggableFactory.java b/src/sqlancer/arangodb/ArangoDBLoggableFactory.java new file mode 100644 index 000000000..927d9f320 --- /dev/null +++ b/src/sqlancer/arangodb/ArangoDBLoggableFactory.java @@ -0,0 +1,40 @@ +package sqlancer.arangodb; + +import java.util.Arrays; + +import sqlancer.common.log.Loggable; +import sqlancer.common.log.LoggableFactory; +import sqlancer.common.log.LoggedString; +import sqlancer.common.query.Query; + +public class ArangoDBLoggableFactory extends LoggableFactory { + @Override + protected Loggable createLoggable(String input, String suffix) { + return new LoggedString(input + suffix); + } + + @Override + public Query getQueryForStateToReproduce(String queryString) { + throw new UnsupportedOperationException(); + } + + @Override + public Query commentOutQuery(Query query) { + throw new UnsupportedOperationException(); + } + + @Override + protected Loggable infoToLoggable(String time, String databaseName, String databaseVersion, long seedValue) { + StringBuilder sb = new StringBuilder(); + sb.append("// Time: ").append(time).append("\n"); + sb.append("// Database: ").append(databaseName).append("\n"); + sb.append("// Database version: ").append(databaseVersion).append("\n"); + sb.append("// seed value: ").append(seedValue).append("\n"); + return new LoggedString(sb.toString()); + } + + @Override + public Loggable convertStacktraceToLoggable(Throwable throwable) { + return new LoggedString(Arrays.toString(throwable.getStackTrace()) + "\n" + throwable.getMessage()); + } +} diff --git a/src/sqlancer/arangodb/ArangoDBOptions.java b/src/sqlancer/arangodb/ArangoDBOptions.java new file mode 100644 index 000000000..04e1038f9 --- /dev/null +++ b/src/sqlancer/arangodb/ArangoDBOptions.java @@ -0,0 +1,41 @@ +package sqlancer.arangodb; + +import static sqlancer.arangodb.ArangoDBOptions.ArangoDBOracleFactory.QUERY_PARTITIONING; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.beust.jcommander.Parameter; + +import sqlancer.DBMSSpecificOptions; +import sqlancer.OracleFactory; +import sqlancer.arangodb.test.ArangoDBQueryPartitioningWhereTester; +import sqlancer.common.oracle.CompositeTestOracle; +import sqlancer.common.oracle.TestOracle; + +public class ArangoDBOptions implements DBMSSpecificOptions { + + @Parameter(names = "--oracle") + public List oracles = Arrays.asList(QUERY_PARTITIONING); + + @Parameter(names = "--test-random-type-inserts", description = "Insert random types instead of schema types.") + public boolean testRandomTypeInserts; + + @Override + public List getTestOracleFactory() { + return oracles; + } + + public enum ArangoDBOracleFactory implements OracleFactory { + QUERY_PARTITIONING { + @Override + public TestOracle create(ArangoDBProvider.ArangoDBGlobalState globalState) throws Exception { + List oracles = new ArrayList<>(); + oracles.add(new ArangoDBQueryPartitioningWhereTester(globalState)); + return new CompositeTestOracle(oracles, globalState); + } + } + + } +} diff --git a/src/sqlancer/arangodb/ArangoDBProvider.java b/src/sqlancer/arangodb/ArangoDBProvider.java new file mode 100644 index 000000000..28160107d --- /dev/null +++ b/src/sqlancer/arangodb/ArangoDBProvider.java @@ -0,0 +1,128 @@ +package sqlancer.arangodb; + +import java.util.ArrayList; +import java.util.List; + +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDatabase; + +import sqlancer.AbstractAction; +import sqlancer.ExecutionTimer; +import sqlancer.GlobalState; +import sqlancer.IgnoreMeException; +import sqlancer.ProviderAdapter; +import sqlancer.Randomly; +import sqlancer.StatementExecutor; +import sqlancer.arangodb.gen.ArangoDBInsertGenerator; +import sqlancer.arangodb.gen.ArangoDBTableGenerator; +import sqlancer.common.log.LoggableFactory; +import sqlancer.common.query.Query; + +public class ArangoDBProvider + extends ProviderAdapter { + + public ArangoDBProvider() { + super(ArangoDBGlobalState.class, ArangoDBOptions.class); + } + + enum Action implements AbstractAction { + INSERT(ArangoDBInsertGenerator::getQuery); + + private final ArangoDBQueryProvider queryProvider; + + Action(ArangoDBQueryProvider queryProvider) { + this.queryProvider = queryProvider; + } + + @Override + public Query getQuery(ArangoDBGlobalState globalState) throws Exception { + return queryProvider.getQuery(globalState); + } + } + + private static int mapActions(ArangoDBGlobalState globalState, Action a) { + Randomly r = globalState.getRandomly(); + switch (a) { + case INSERT: + return r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); + default: + throw new AssertionError(a); + } + } + + public static class ArangoDBGlobalState extends GlobalState { + + private final List schemaTables = new ArrayList<>(); + + public synchronized void addTable(ArangoDBSchema.ArangoDBTable table) { + schemaTables.add(table); + } + + @Override + protected void executeEpilogue(Query q, boolean success, ExecutionTimer timer) throws Exception { + boolean logExecutionTime = getOptions().logExecutionTime(); + if (success && getOptions().printSucceedingStatements()) { + System.out.println(q.getLogString()); + } + if (logExecutionTime) { + getLogger().writeCurrent("//" + timer.end().asString()); + } + if (q.couldAffectSchema()) { + updateSchema(); + } + } + + @Override + protected ArangoDBSchema readSchema() throws Exception { + return new ArangoDBSchema(schemaTables); + } + } + + @Override + protected void checkViewsAreValid(ArangoDBGlobalState globalState) { + + } + + @Override + public void generateDatabase(ArangoDBGlobalState globalState) throws Exception { + for (int i = 0; i < Randomly.fromOptions(4, 5, 6); i++) { + boolean success; + do { + ArangoDBQueryAdapter queryAdapter = new ArangoDBTableGenerator().getQuery(globalState); + success = globalState.executeStatement(queryAdapter); + } while (!success); + } + StatementExecutor se = new StatementExecutor<>(globalState, Action.values(), + ArangoDBProvider::mapActions, (q) -> { + if (globalState.getSchema().getDatabaseTables().isEmpty()) { + throw new IgnoreMeException(); + } + }); + se.executeStatements(); + } + + @Override + public ArangoDBConnection createDatabase(ArangoDBGlobalState globalState) throws Exception { + ArangoDB arangoDB = new ArangoDB.Builder().user(globalState.getOptions().getUserName()) + .password(globalState.getOptions().getPassword()).build(); + ArangoDatabase database = arangoDB.db(globalState.getDatabaseName()); + try { + database.drop(); + } catch (Exception ignored) { + + } + arangoDB.createDatabase(globalState.getDatabaseName()); + database = arangoDB.db(globalState.getDatabaseName()); + return new ArangoDBConnection(arangoDB, database); + } + + @Override + public String getDBMSName() { + return "arangodb"; + } + + @Override + public LoggableFactory getLoggableFactory() { + return new ArangoDBLoggableFactory(); + } +} diff --git a/src/sqlancer/arangodb/ArangoDBQueryAdapter.java b/src/sqlancer/arangodb/ArangoDBQueryAdapter.java new file mode 100644 index 000000000..2d1b77af0 --- /dev/null +++ b/src/sqlancer/arangodb/ArangoDBQueryAdapter.java @@ -0,0 +1,15 @@ +package sqlancer.arangodb; + +import sqlancer.common.query.Query; + +public abstract class ArangoDBQueryAdapter extends Query { + @Override + public String getQueryString() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUnterminatedQueryString() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/sqlancer/arangodb/ArangoDBQueryProvider.java b/src/sqlancer/arangodb/ArangoDBQueryProvider.java new file mode 100644 index 000000000..94a4ffda3 --- /dev/null +++ b/src/sqlancer/arangodb/ArangoDBQueryProvider.java @@ -0,0 +1,6 @@ +package sqlancer.arangodb; + +@FunctionalInterface +public interface ArangoDBQueryProvider { + ArangoDBQueryAdapter getQuery(S globalState) throws Exception; +} diff --git a/src/sqlancer/arangodb/ArangoDBSchema.java b/src/sqlancer/arangodb/ArangoDBSchema.java new file mode 100644 index 000000000..e3ceb3586 --- /dev/null +++ b/src/sqlancer/arangodb/ArangoDBSchema.java @@ -0,0 +1,66 @@ +package sqlancer.arangodb; + +import java.util.Collections; +import java.util.List; + +import sqlancer.Randomly; +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 ArangoDBSchema extends AbstractSchema { + + public enum ArangoDBDataType { + INTEGER, DOUBLE, STRING, BOOLEAN; + + public static ArangoDBDataType getRandom() { + return Randomly.fromOptions(values()); + } + } + + public static class ArangoDBColumn extends AbstractTableColumn { + + private final boolean isId; + private final boolean isNullable; + + public ArangoDBColumn(String name, ArangoDBDataType type, boolean isId, boolean isNullable) { + super(name, null, type); + this.isId = isId; + this.isNullable = isNullable; + } + + public boolean isId() { + return isId; + } + + public boolean isNullable() { + return isNullable; + } + } + + public ArangoDBSchema(List databaseTables) { + super(databaseTables); + } + + public static class ArangoDBTables extends AbstractTables { + + public ArangoDBTables(List tables) { + super(tables); + } + } + + public static class ArangoDBTable + extends AbstractTable { + + public ArangoDBTable(String name, List columns, boolean isView) { + super(name, columns, Collections.emptyList(), isView); + } + + @Override + public long getNrRows(ArangoDBProvider.ArangoDBGlobalState globalState) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/sqlancer/arangodb/ast/ArangoDBConstant.java b/src/sqlancer/arangodb/ast/ArangoDBConstant.java new file mode 100644 index 000000000..cb60761c8 --- /dev/null +++ b/src/sqlancer/arangodb/ast/ArangoDBConstant.java @@ -0,0 +1,126 @@ +package sqlancer.arangodb.ast; + +import com.arangodb.entity.BaseDocument; + +import sqlancer.common.ast.newast.Node; + +public abstract class ArangoDBConstant implements Node { + private ArangoDBConstant() { + + } + + public abstract void setValueInDocument(BaseDocument document, String key); + + public abstract void getLogValue(); + + public abstract Object getValue(); + + public static class ArangoDBIntegerConstant extends ArangoDBConstant { + + private final int value; + + public ArangoDBIntegerConstant(int value) { + this.value = value; + } + + @Override + public void setValueInDocument(BaseDocument document, String key) { + document.addAttribute(key, value); + } + + @Override + public void getLogValue() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getValue() { + return value; + } + } + + public static Node createIntegerConstant(int value) { + return new ArangoDBIntegerConstant(value); + } + + public static class ArangoDBStringConstant extends ArangoDBConstant { + private final String value; + + public ArangoDBStringConstant(String value) { + this.value = value; + } + + @Override + public void setValueInDocument(BaseDocument document, String key) { + document.addAttribute(key, value); + } + + @Override + public void getLogValue() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getValue() { + return value; + } + } + + public static Node createStringConstant(String value) { + return new ArangoDBStringConstant(value); + } + + public static class ArangoDBBooleanConstant extends ArangoDBConstant { + private final boolean value; + + public ArangoDBBooleanConstant(boolean value) { + this.value = value; + } + + @Override + public void setValueInDocument(BaseDocument document, String key) { + document.addAttribute(key, value); + } + + @Override + public void getLogValue() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getValue() { + return value; + } + } + + public static Node createBooleanConstant(boolean value) { + return new ArangoDBBooleanConstant(value); + } + + public static class ArangoDBDoubleConstant extends ArangoDBConstant { + private final double value; + + public ArangoDBDoubleConstant(double value) { + this.value = value; + } + + @Override + public void setValueInDocument(BaseDocument document, String key) { + document.addAttribute(key, value); + } + + @Override + public void getLogValue() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getValue() { + return value; + } + } + + public static Node createDoubleConstant(double value) { + return new ArangoDBDoubleConstant(value); + } +} diff --git a/src/sqlancer/arangodb/ast/ArangoDBExpression.java b/src/sqlancer/arangodb/ast/ArangoDBExpression.java new file mode 100644 index 000000000..facbbfe9e --- /dev/null +++ b/src/sqlancer/arangodb/ast/ArangoDBExpression.java @@ -0,0 +1,4 @@ +package sqlancer.arangodb.ast; + +public interface ArangoDBExpression { +} diff --git a/src/sqlancer/arangodb/gen/ArangoDBInsertGenerator.java b/src/sqlancer/arangodb/gen/ArangoDBInsertGenerator.java new file mode 100644 index 000000000..9a27ccd57 --- /dev/null +++ b/src/sqlancer/arangodb/gen/ArangoDBInsertGenerator.java @@ -0,0 +1,39 @@ +package sqlancer.arangodb.gen; + +import com.arangodb.entity.BaseDocument; + +import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.ArangoDBQueryAdapter; +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.arangodb.query.ArangoDBConstantGenerator; +import sqlancer.arangodb.query.ArangoDBInsertQuery; + +public final class ArangoDBInsertGenerator { + + private final ArangoDBProvider.ArangoDBGlobalState globalState; + + private ArangoDBInsertGenerator(ArangoDBProvider.ArangoDBGlobalState globalState) { + this.globalState = globalState; + } + + public static ArangoDBQueryAdapter getQuery(ArangoDBProvider.ArangoDBGlobalState globalState) { + return new ArangoDBInsertGenerator(globalState).generate(); + } + + private ArangoDBQueryAdapter generate() { + BaseDocument result = new BaseDocument(); + ArangoDBSchema.ArangoDBTable table = globalState.getSchema().getRandomTable(); + ArangoDBConstantGenerator constantGenerator = new ArangoDBConstantGenerator(globalState); + + for (int i = 0; i < table.getColumns().size(); i++) { + if (!globalState.getDmbsSpecificOptions().testRandomTypeInserts) { + constantGenerator.addRandomConstantWithType(result, table.getColumns().get(i).getName(), + table.getColumns().get(i).getType()); + } else { + constantGenerator.addRandomConstant(result, table.getColumns().get(i).getName()); + } + } + + return new ArangoDBInsertQuery(table, result); + } +} diff --git a/src/sqlancer/arangodb/gen/ArangoDBTableGenerator.java b/src/sqlancer/arangodb/gen/ArangoDBTableGenerator.java new file mode 100644 index 000000000..1236c3ce4 --- /dev/null +++ b/src/sqlancer/arangodb/gen/ArangoDBTableGenerator.java @@ -0,0 +1,44 @@ +package sqlancer.arangodb.gen; + +import java.util.ArrayList; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.ArangoDBQueryAdapter; +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.arangodb.query.ArangoDBCreateTableQuery; + +public class ArangoDBTableGenerator { + + private ArangoDBSchema.ArangoDBTable table; + private final List columnsToBeAdded = new ArrayList<>(); + + public ArangoDBQueryAdapter getQuery(ArangoDBProvider.ArangoDBGlobalState globalState) { + String tableName = globalState.getSchema().getFreeTableName(); + ArangoDBCreateTableQuery createTableQuery = new ArangoDBCreateTableQuery(tableName); + table = new ArangoDBSchema.ArangoDBTable(tableName, columnsToBeAdded, false); + for (int i = 0; i < Randomly.smallNumber() + 1; i++) { + String columnName = String.format("c%d", i); + createColumn(columnName); + } + globalState.addTable(table); + return createTableQuery; + } + + private ArangoDBSchema.ArangoDBDataType createColumn(String columnName) { + ArangoDBSchema.ArangoDBDataType dataType = ArangoDBSchema.ArangoDBDataType.getRandom(); + ArangoDBSchema.ArangoDBColumn newColumn = new ArangoDBSchema.ArangoDBColumn(columnName, dataType, false, false); + newColumn.setTable(table); + columnsToBeAdded.add(newColumn); + return dataType; + } + + public String getTableName() { + return table.getName(); + } + + public ArangoDBSchema.ArangoDBTable getGeneratedTable() { + return table; + } +} diff --git a/src/sqlancer/arangodb/query/ArangoDBConstantGenerator.java b/src/sqlancer/arangodb/query/ArangoDBConstantGenerator.java new file mode 100644 index 000000000..406e8adca --- /dev/null +++ b/src/sqlancer/arangodb/query/ArangoDBConstantGenerator.java @@ -0,0 +1,46 @@ +package sqlancer.arangodb.query; + +import com.arangodb.entity.BaseDocument; + +import sqlancer.Randomly; +import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.arangodb.ast.ArangoDBConstant; + +public class ArangoDBConstantGenerator { + private final ArangoDBProvider.ArangoDBGlobalState globalState; + + public ArangoDBConstantGenerator(ArangoDBProvider.ArangoDBGlobalState globalState) { + this.globalState = globalState; + } + + public void addRandomConstant(BaseDocument document, String key) { + ArangoDBSchema.ArangoDBDataType type = ArangoDBSchema.ArangoDBDataType.getRandom(); + addRandomConstantWithType(document, key, type); + } + + public void addRandomConstantWithType(BaseDocument document, String key, ArangoDBSchema.ArangoDBDataType dataType) { + ArangoDBConstant constant; + switch (dataType) { + case STRING: + constant = new ArangoDBConstant.ArangoDBStringConstant(globalState.getRandomly().getString()); + constant.setValueInDocument(document, key); + return; + case DOUBLE: + constant = new ArangoDBConstant.ArangoDBDoubleConstant(globalState.getRandomly().getDouble()); + constant.setValueInDocument(document, key); + return; + case BOOLEAN: + constant = new ArangoDBConstant.ArangoDBBooleanConstant(Randomly.getBoolean()); + constant.setValueInDocument(document, key); + return; + case INTEGER: + constant = new ArangoDBConstant.ArangoDBIntegerConstant((int) globalState.getRandomly().getInteger()); + constant.setValueInDocument(document, key); + return; + default: + throw new AssertionError(dataType); + } + + } +} diff --git a/src/sqlancer/arangodb/query/ArangoDBCreateTableQuery.java b/src/sqlancer/arangodb/query/ArangoDBCreateTableQuery.java new file mode 100644 index 000000000..888c5d653 --- /dev/null +++ b/src/sqlancer/arangodb/query/ArangoDBCreateTableQuery.java @@ -0,0 +1,45 @@ +package sqlancer.arangodb.query; + +import sqlancer.GlobalState; +import sqlancer.Main; +import sqlancer.arangodb.ArangoDBConnection; +import sqlancer.arangodb.ArangoDBQueryAdapter; +import sqlancer.common.query.ExpectedErrors; + +public class ArangoDBCreateTableQuery extends ArangoDBQueryAdapter { + + private final String tableName; + + public ArangoDBCreateTableQuery(String tableName) { + this.tableName = tableName; + } + + @Override + public boolean couldAffectSchema() { + return true; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + try { + globalState.getConnection().getDatabase().createCollection(tableName); + Main.nrSuccessfulActions.addAndGet(1); + return true; + } catch (Exception e) { + Main.nrUnsuccessfulActions.addAndGet(1); + throw e; + } + } + + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } + + @Override + public String getLogString() { + // TODO Patrick + return ""; + } +} diff --git a/src/sqlancer/arangodb/query/ArangoDBInsertQuery.java b/src/sqlancer/arangodb/query/ArangoDBInsertQuery.java new file mode 100644 index 000000000..327ccb99a --- /dev/null +++ b/src/sqlancer/arangodb/query/ArangoDBInsertQuery.java @@ -0,0 +1,50 @@ +package sqlancer.arangodb.query; + +import com.arangodb.entity.BaseDocument; + +import sqlancer.GlobalState; +import sqlancer.Main; +import sqlancer.arangodb.ArangoDBConnection; +import sqlancer.arangodb.ArangoDBQueryAdapter; +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.common.query.ExpectedErrors; + +public class ArangoDBInsertQuery extends ArangoDBQueryAdapter { + + private final ArangoDBSchema.ArangoDBTable table; + private final BaseDocument documentToBeInserted; + + public ArangoDBInsertQuery(ArangoDBSchema.ArangoDBTable table, BaseDocument documentToBeInserted) { + this.table = table; + this.documentToBeInserted = documentToBeInserted; + } + + @Override + public boolean couldAffectSchema() { + return true; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + try { + globalState.getConnection().getDatabase().collection(table.getName()).insertDocument(documentToBeInserted); + Main.nrSuccessfulActions.addAndGet(1); + return true; + } catch (Exception e) { + Main.nrUnsuccessfulActions.addAndGet(1); + throw e; + } + } + + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } + + @Override + public String getLogString() { + // TODO Patrick + return ""; + } +} diff --git a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java new file mode 100644 index 000000000..36768d01e --- /dev/null +++ b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java @@ -0,0 +1,27 @@ +package sqlancer.arangodb.test; + +import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.ast.ArangoDBExpression; +import sqlancer.common.ast.newast.Node; +import sqlancer.common.gen.ExpressionGenerator; +import sqlancer.common.oracle.TernaryLogicPartitioningOracleBase; +import sqlancer.common.oracle.TestOracle; + +public class ArangoDBQueryPartitioningBase + extends TernaryLogicPartitioningOracleBase, ArangoDBProvider.ArangoDBGlobalState> + implements TestOracle { + + protected ArangoDBQueryPartitioningBase(ArangoDBProvider.ArangoDBGlobalState state) { + super(state); + } + + @Override + protected ExpressionGenerator> getGen() { + throw new UnsupportedOperationException(); + } + + @Override + public void check() throws Exception { + + } +} diff --git a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningWhereTester.java b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningWhereTester.java new file mode 100644 index 000000000..cbd380366 --- /dev/null +++ b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningWhereTester.java @@ -0,0 +1,9 @@ +package sqlancer.arangodb.test; + +import sqlancer.arangodb.ArangoDBProvider; + +public class ArangoDBQueryPartitioningWhereTester extends ArangoDBQueryPartitioningBase { + public ArangoDBQueryPartitioningWhereTester(ArangoDBProvider.ArangoDBGlobalState state) { + super(state); + } +} From 66919d4bf271ebf022cb0ee3833e9f5170bc6152 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 28 Feb 2021 16:59:55 +0100 Subject: [PATCH 16/24] Complete log implementation for inserts and collection creation --- .../query/ArangoDBCreateTableQuery.java | 3 +-- .../arangodb/query/ArangoDBInsertQuery.java | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/sqlancer/arangodb/query/ArangoDBCreateTableQuery.java b/src/sqlancer/arangodb/query/ArangoDBCreateTableQuery.java index 888c5d653..00b3276d0 100644 --- a/src/sqlancer/arangodb/query/ArangoDBCreateTableQuery.java +++ b/src/sqlancer/arangodb/query/ArangoDBCreateTableQuery.java @@ -39,7 +39,6 @@ public ExpectedErrors getExpectedErrors() { @Override public String getLogString() { - // TODO Patrick - return ""; + return "db._create(\"" + tableName + "\")"; } } diff --git a/src/sqlancer/arangodb/query/ArangoDBInsertQuery.java b/src/sqlancer/arangodb/query/ArangoDBInsertQuery.java index 327ccb99a..9a3612062 100644 --- a/src/sqlancer/arangodb/query/ArangoDBInsertQuery.java +++ b/src/sqlancer/arangodb/query/ArangoDBInsertQuery.java @@ -1,5 +1,7 @@ package sqlancer.arangodb.query; +import java.util.Map; + import com.arangodb.entity.BaseDocument; import sqlancer.GlobalState; @@ -44,7 +46,21 @@ public ExpectedErrors getExpectedErrors() { @Override public String getLogString() { - // TODO Patrick - return ""; + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("db._query(\"INSERT { "); + String filler = ""; + for (Map.Entry stringObjectEntry : documentToBeInserted.getProperties().entrySet()) { + stringBuilder.append(filler); + filler = ", "; + stringBuilder.append(stringObjectEntry.getKey()).append(": "); + Object value = stringObjectEntry.getValue(); + if (value instanceof String) { + stringBuilder.append("'").append(value).append("'"); + } else { + stringBuilder.append(value); + } + } + stringBuilder.append("} IN ").append(table.getName()).append("\")"); + return stringBuilder.toString(); } } From ec3f710a98e1f810b7b4d312e6483051214e1483 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 2 Mar 2021 15:47:31 +0100 Subject: [PATCH 17/24] Implement ternary partitioning logic where tester for ArangoDB ArangoDB now randomly generates queries that support binary comparison, binary logic operations such as or/and and the unary prefix not. Also everything is logged and the results are checked with the new ComparatorHelper. --- .../arangodb/ArangoDBComparatorHelper.java | 73 +++++++++ src/sqlancer/arangodb/ArangoDBSchema.java | 4 + .../arangodb/ast/ArangoDBConstant.java | 30 +--- src/sqlancer/arangodb/ast/ArangoDBSelect.java | 59 ++++++++ .../ast/ArangoDBUnsupportedPredicate.java | 6 + .../ArangoDBFilterExpressionGenerator.java | 139 ++++++++++++++++++ .../arangodb/query/ArangoDBSelectQuery.java | 65 ++++++++ .../test/ArangoDBQueryPartitioningBase.java | 26 +++- .../ArangoDBQueryPartitioningWhereTester.java | 29 ++++ .../visitor/ArangoDBToQueryVisitor.java | 92 ++++++++++++ .../arangodb/visitor/ArangoDBVisitor.java | 47 ++++++ 11 files changed, 545 insertions(+), 25 deletions(-) create mode 100644 src/sqlancer/arangodb/ArangoDBComparatorHelper.java create mode 100644 src/sqlancer/arangodb/ast/ArangoDBSelect.java create mode 100644 src/sqlancer/arangodb/ast/ArangoDBUnsupportedPredicate.java create mode 100644 src/sqlancer/arangodb/gen/ArangoDBFilterExpressionGenerator.java create mode 100644 src/sqlancer/arangodb/query/ArangoDBSelectQuery.java create mode 100644 src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java create mode 100644 src/sqlancer/arangodb/visitor/ArangoDBVisitor.java diff --git a/src/sqlancer/arangodb/ArangoDBComparatorHelper.java b/src/sqlancer/arangodb/ArangoDBComparatorHelper.java new file mode 100644 index 000000000..70cc451fe --- /dev/null +++ b/src/sqlancer/arangodb/ArangoDBComparatorHelper.java @@ -0,0 +1,73 @@ +package sqlancer.arangodb; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.arangodb.entity.BaseDocument; + +import sqlancer.IgnoreMeException; +import sqlancer.Main; +import sqlancer.arangodb.query.ArangoDBSelectQuery; +import sqlancer.common.query.ExpectedErrors; + +public final class ArangoDBComparatorHelper { + + private ArangoDBComparatorHelper() { + + } + + public static List getResultSetAsDocumentList(ArangoDBSelectQuery query, + ArangoDBProvider.ArangoDBGlobalState state) throws Exception { + ExpectedErrors errors = query.getExpectedErrors(); + List result; + try { + query.executeAndGet(state); + Main.nrSuccessfulActions.addAndGet(1); + result = query.getResultSet(); + return result; + } catch (Exception e) { + Main.nrUnsuccessfulActions.addAndGet(1); + if (e instanceof IgnoreMeException) { + throw e; + } + if (e.getMessage() == null) { + throw new AssertionError(query.getLogString(), e); + } + if (errors.errorIsExpected(e.getMessage())) { + throw new IgnoreMeException(); + } + throw new AssertionError(query.getLogString(), e); + } + + } + + public static void assumeResultSetsAreEqual(List resultSet, List secondResultSet, + ArangoDBSelectQuery originalQuery) { + if (resultSet.size() != secondResultSet.size()) { + String assertionMessage = String.format("The Size of the result sets mismatch (%d and %d)!\n%s", + resultSet.size(), secondResultSet.size(), originalQuery.getLogString()); + 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); + StringBuilder firstMisses = new StringBuilder(); + for (BaseDocument document : firstResultSetMisses) { + firstMisses.append(document).append(" "); + } + StringBuilder secondMisses = new StringBuilder(); + for (BaseDocument document : secondResultSetMisses) { + secondMisses.append(document).append(" "); + } + String assertMessage = String.format("The Content of the result sets mismatch!\n %s \n %s\n %s", + firstMisses.toString(), secondMisses.toString(), originalQuery.getLogString()); + throw new AssertionError(assertMessage); + } + } +} diff --git a/src/sqlancer/arangodb/ArangoDBSchema.java b/src/sqlancer/arangodb/ArangoDBSchema.java index e3ceb3586..35e251b8b 100644 --- a/src/sqlancer/arangodb/ArangoDBSchema.java +++ b/src/sqlancer/arangodb/ArangoDBSchema.java @@ -63,4 +63,8 @@ public long getNrRows(ArangoDBProvider.ArangoDBGlobalState globalState) { throw new UnsupportedOperationException(); } } + + public ArangoDBTables getRandomTableNonEmptyTables() { + return new ArangoDBTables(Randomly.nonEmptySubset(getDatabaseTables())); + } } diff --git a/src/sqlancer/arangodb/ast/ArangoDBConstant.java b/src/sqlancer/arangodb/ast/ArangoDBConstant.java index cb60761c8..351dbd822 100644 --- a/src/sqlancer/arangodb/ast/ArangoDBConstant.java +++ b/src/sqlancer/arangodb/ast/ArangoDBConstant.java @@ -11,8 +11,6 @@ private ArangoDBConstant() { public abstract void setValueInDocument(BaseDocument document, String key); - public abstract void getLogValue(); - public abstract Object getValue(); public static class ArangoDBIntegerConstant extends ArangoDBConstant { @@ -28,11 +26,6 @@ public void setValueInDocument(BaseDocument document, String key) { document.addAttribute(key, value); } - @Override - public void getLogValue() { - throw new UnsupportedOperationException(); - } - @Override public Object getValue() { return value; @@ -55,14 +48,9 @@ public void setValueInDocument(BaseDocument document, String key) { document.addAttribute(key, value); } - @Override - public void getLogValue() { - throw new UnsupportedOperationException(); - } - @Override public Object getValue() { - return value; + return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"; } } @@ -82,11 +70,6 @@ public void setValueInDocument(BaseDocument document, String key) { document.addAttribute(key, value); } - @Override - public void getLogValue() { - throw new UnsupportedOperationException(); - } - @Override public Object getValue() { return value; @@ -101,7 +84,11 @@ public static class ArangoDBDoubleConstant extends ArangoDBConstant { private final double value; public ArangoDBDoubleConstant(double value) { - this.value = value; + if (Double.isInfinite(value) || Double.isNaN(value)) { + this.value = 0.0; + } else { + this.value = value; + } } @Override @@ -109,11 +96,6 @@ public void setValueInDocument(BaseDocument document, String key) { document.addAttribute(key, value); } - @Override - public void getLogValue() { - throw new UnsupportedOperationException(); - } - @Override public Object getValue() { return value; diff --git a/src/sqlancer/arangodb/ast/ArangoDBSelect.java b/src/sqlancer/arangodb/ast/ArangoDBSelect.java new file mode 100644 index 000000000..cbba174c1 --- /dev/null +++ b/src/sqlancer/arangodb/ast/ArangoDBSelect.java @@ -0,0 +1,59 @@ +package sqlancer.arangodb.ast; + +import java.util.List; + +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.common.ast.newast.Node; + +public class ArangoDBSelect implements Node { + private List fromColumns; + private List projectionColumns; + private boolean hasFilter; + private Node filterClause; + + public List getFromColumns() { + if (fromColumns == null || fromColumns.isEmpty()) { + throw new IllegalStateException(); + } + return fromColumns; + } + + public void setFromColumns(List fromColumns) { + if (fromColumns == null || fromColumns.isEmpty()) { + throw new IllegalStateException(); + } + this.fromColumns = fromColumns; + } + + public List getProjectionColumns() { + if (projectionColumns == null) { + throw new IllegalStateException(); + } + return projectionColumns; + } + + public void setProjectionColumns(List projectionColumns) { + if (projectionColumns == null) { + throw new IllegalStateException(); + } + this.projectionColumns = projectionColumns; + } + + public void setFilterClause(Node filterClause) { + if (filterClause == null) { + hasFilter = false; + this.filterClause = null; + return; + } + hasFilter = true; + this.filterClause = filterClause; + } + + public Node getFilterClause() { + return filterClause; + } + + public boolean hasFilter() { + return hasFilter; + } +} diff --git a/src/sqlancer/arangodb/ast/ArangoDBUnsupportedPredicate.java b/src/sqlancer/arangodb/ast/ArangoDBUnsupportedPredicate.java new file mode 100644 index 000000000..eabd25578 --- /dev/null +++ b/src/sqlancer/arangodb/ast/ArangoDBUnsupportedPredicate.java @@ -0,0 +1,6 @@ +package sqlancer.arangodb.ast; + +import sqlancer.common.ast.newast.Node; + +public class ArangoDBUnsupportedPredicate implements Node { +} diff --git a/src/sqlancer/arangodb/gen/ArangoDBFilterExpressionGenerator.java b/src/sqlancer/arangodb/gen/ArangoDBFilterExpressionGenerator.java new file mode 100644 index 000000000..902a33c68 --- /dev/null +++ b/src/sqlancer/arangodb/gen/ArangoDBFilterExpressionGenerator.java @@ -0,0 +1,139 @@ +package sqlancer.arangodb.gen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.arangodb.ast.ArangoDBConstant; +import sqlancer.arangodb.ast.ArangoDBExpression; +import sqlancer.arangodb.ast.ArangoDBUnsupportedPredicate; +import sqlancer.common.ast.BinaryOperatorNode; +import sqlancer.common.ast.newast.ColumnReferenceNode; +import sqlancer.common.ast.newast.NewBinaryOperatorNode; +import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; +import sqlancer.common.ast.newast.Node; +import sqlancer.common.gen.UntypedExpressionGenerator; + +public class ArangoDBFilterExpressionGenerator + extends UntypedExpressionGenerator, ArangoDBSchema.ArangoDBColumn> { + + private final ArangoDBProvider.ArangoDBGlobalState globalState; + + private enum Expression { + BINARY_LOGICAL, UNARY_PREFIX, BINARY_COMPARISON + } + + public ArangoDBFilterExpressionGenerator(ArangoDBProvider.ArangoDBGlobalState globalState) { + this.globalState = globalState; + } + + @Override + public Node generateConstant() { + ArangoDBSchema.ArangoDBDataType dataType = ArangoDBSchema.ArangoDBDataType.getRandom(); + switch (dataType) { + case INTEGER: + return ArangoDBConstant.createIntegerConstant((int) globalState.getRandomly().getInteger()); + case BOOLEAN: + return ArangoDBConstant.createBooleanConstant(Randomly.getBoolean()); + case DOUBLE: + return ArangoDBConstant.createDoubleConstant(globalState.getRandomly().getDouble()); + case STRING: + return ArangoDBConstant.createStringConstant(globalState.getRandomly().getString()); + default: + throw new AssertionError(dataType); + } + } + + @Override + protected Node generateExpression(int depth) { + if (depth >= globalState.getOptions().getMaxExpressionDepth() || Randomly.getBoolean()) { + return generateLeafNode(); + } + List possibleOptions = new ArrayList<>(Arrays.asList(Expression.values())); + Expression expression = Randomly.fromList(possibleOptions); + switch (expression) { + case BINARY_COMPARISON: + BinaryOperatorNode.Operator op = ArangoDBBinaryComparisonOperator.getRandom(); + return new NewBinaryOperatorNode<>(generateExpression(depth + 1), generateExpression(depth + 1), op); + case UNARY_PREFIX: + return new NewUnaryPrefixOperatorNode<>(generateExpression(depth + 1), + ArangoDBUnaryPrefixOperator.getRandom()); + case BINARY_LOGICAL: + op = ArangoDBBinaryLogicalOperator.getRandom(); + return new NewBinaryOperatorNode<>(generateExpression(depth + 1), generateExpression(depth + 1), op); + default: + throw new AssertionError(expression); + } + } + + @Override + protected Node generateColumn() { + ArangoDBSchema.ArangoDBColumn column = Randomly.fromList(columns); + return new ColumnReferenceNode<>(column); + } + + @Override + public Node negatePredicate(Node predicate) { + return new NewUnaryPrefixOperatorNode<>(predicate, ArangoDBUnaryPrefixOperator.NOT); + } + + @Override + public Node isNull(Node expr) { + return new ArangoDBUnsupportedPredicate<>(); + } + + public enum ArangoDBBinaryComparisonOperator implements BinaryOperatorNode.Operator { + EQUALS("=="), NOT_EQUALS("!="), LESS_THAN("<"), LESS_OR_EQUAL("<="), GREATER_THAN(">"), GREATER_OR_EQUAL(">="); + + private final String representation; + + ArangoDBBinaryComparisonOperator(String representation) { + this.representation = representation; + } + + @Override + public String getTextRepresentation() { + return representation; + } + + public static ArangoDBBinaryComparisonOperator getRandom() { + return Randomly.fromOptions(values()); + } + } + + public enum ArangoDBUnaryPrefixOperator implements BinaryOperatorNode.Operator { + NOT("!"); + + private final String representation; + + ArangoDBUnaryPrefixOperator(String representation) { + this.representation = representation; + } + + @Override + public String getTextRepresentation() { + return representation; + } + + public static ArangoDBUnaryPrefixOperator getRandom() { + return Randomly.fromOptions(values()); + } + } + + public enum ArangoDBBinaryLogicalOperator implements BinaryOperatorNode.Operator { + AND, OR; + + @Override + public String getTextRepresentation() { + return toString(); + } + + public static BinaryOperatorNode.Operator getRandom() { + return Randomly.fromOptions(values()); + } + } + +} diff --git a/src/sqlancer/arangodb/query/ArangoDBSelectQuery.java b/src/sqlancer/arangodb/query/ArangoDBSelectQuery.java new file mode 100644 index 000000000..400585ca2 --- /dev/null +++ b/src/sqlancer/arangodb/query/ArangoDBSelectQuery.java @@ -0,0 +1,65 @@ +package sqlancer.arangodb.query; + +import java.io.IOException; +import java.util.List; + +import com.arangodb.ArangoCursor; +import com.arangodb.entity.BaseDocument; + +import sqlancer.GlobalState; +import sqlancer.arangodb.ArangoDBConnection; +import sqlancer.arangodb.ArangoDBQueryAdapter; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.SQLancerResultSet; + +public class ArangoDBSelectQuery extends ArangoDBQueryAdapter { + + private final String query; + + private List resultSet; + + public ArangoDBSelectQuery(String query) { + this.query = query; + } + + @Override + public boolean couldAffectSchema() { + return false; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } + + @Override + public String getLogString() { + return "db._query(\"" + query + "\")"; + } + + @Override + public > SQLancerResultSet executeAndGet(G globalState, + String... fills) throws Exception { + if (globalState.getOptions().logEachSelect()) { + globalState.getLogger().writeCurrent(this.getLogString()); + try { + globalState.getLogger().getCurrentFileWriter().flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + ArangoCursor cursor = globalState.getConnection().getDatabase().query(query, BaseDocument.class); + resultSet = cursor.asListRemaining(); + return null; + } + + public List getResultSet() { + return resultSet; + } +} diff --git a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java index 36768d01e..97c5cc154 100644 --- a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java +++ b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java @@ -1,7 +1,13 @@ package sqlancer.arangodb.test; +import java.util.List; + +import sqlancer.Randomly; import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.ArangoDBSchema; import sqlancer.arangodb.ast.ArangoDBExpression; +import sqlancer.arangodb.ast.ArangoDBSelect; +import sqlancer.arangodb.gen.ArangoDBFilterExpressionGenerator; import sqlancer.common.ast.newast.Node; import sqlancer.common.gen.ExpressionGenerator; import sqlancer.common.oracle.TernaryLogicPartitioningOracleBase; @@ -11,17 +17,35 @@ public class ArangoDBQueryPartitioningBase extends TernaryLogicPartitioningOracleBase, ArangoDBProvider.ArangoDBGlobalState> implements TestOracle { + protected ArangoDBSchema schema; + protected List targetColumns; + protected ArangoDBFilterExpressionGenerator expressionGenerator; + protected ArangoDBSelect select; + protected ArangoDBQueryPartitioningBase(ArangoDBProvider.ArangoDBGlobalState state) { super(state); } @Override protected ExpressionGenerator> getGen() { - throw new UnsupportedOperationException(); + return expressionGenerator; } @Override public void check() throws Exception { + schema = state.getSchema(); + generateTargetColumns(); + expressionGenerator = new ArangoDBFilterExpressionGenerator(state).setColumns(targetColumns); + initializeTernaryPredicateVariants(); + select = new ArangoDBSelect<>(); + select.setFromColumns(targetColumns); + select.setProjectionColumns(Randomly.nonEmptySubset(targetColumns)); + } + private void generateTargetColumns() { + ArangoDBSchema.ArangoDBTables targetTables; + targetTables = schema.getRandomTableNonEmptyTables(); + List allColumns = targetTables.getColumns(); + targetColumns = Randomly.nonEmptySubset(allColumns); } } diff --git a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningWhereTester.java b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningWhereTester.java index cbd380366..80b7d46bf 100644 --- a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningWhereTester.java +++ b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningWhereTester.java @@ -1,9 +1,38 @@ package sqlancer.arangodb.test; +import static sqlancer.arangodb.ArangoDBComparatorHelper.assumeResultSetsAreEqual; +import static sqlancer.arangodb.ArangoDBComparatorHelper.getResultSetAsDocumentList; + +import java.util.List; + +import com.arangodb.entity.BaseDocument; + import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.query.ArangoDBSelectQuery; +import sqlancer.arangodb.visitor.ArangoDBVisitor; public class ArangoDBQueryPartitioningWhereTester extends ArangoDBQueryPartitioningBase { public ArangoDBQueryPartitioningWhereTester(ArangoDBProvider.ArangoDBGlobalState state) { super(state); } + + @Override + public void check() throws Exception { + super.check(); + select.setFilterClause(null); + + ArangoDBSelectQuery query = ArangoDBVisitor.asSelectQuery(select); + List firstResultSet = getResultSetAsDocumentList(query, state); + + select.setFilterClause(predicate); + query = ArangoDBVisitor.asSelectQuery(select); + List secondResultSet = getResultSetAsDocumentList(query, state); + + select.setFilterClause(negatedPredicate); + query = ArangoDBVisitor.asSelectQuery(select); + List thirdResultSet = getResultSetAsDocumentList(query, state); + + secondResultSet.addAll(thirdResultSet); + assumeResultSetsAreEqual(firstResultSet, secondResultSet, query); + } } diff --git a/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java b/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java new file mode 100644 index 000000000..160249df8 --- /dev/null +++ b/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java @@ -0,0 +1,92 @@ +package sqlancer.arangodb.visitor; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.arangodb.ast.ArangoDBConstant; +import sqlancer.arangodb.ast.ArangoDBExpression; +import sqlancer.arangodb.ast.ArangoDBSelect; +import sqlancer.arangodb.query.ArangoDBSelectQuery; +import sqlancer.common.ast.newast.ColumnReferenceNode; +import sqlancer.common.ast.newast.NewBinaryOperatorNode; +import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; + +public class ArangoDBToQueryVisitor extends ArangoDBVisitor { + + private final StringBuilder stringBuilder; + + public ArangoDBToQueryVisitor() { + stringBuilder = new StringBuilder(); + } + + @Override + protected void visit(ArangoDBSelect expression) { + generateFrom(expression); + if (expression.hasFilter()) { + stringBuilder.append("FILTER "); + visit(expression.getFilterClause()); + stringBuilder.append(" "); + } + generateProject(expression); + + } + + @Override + protected void visit(ColumnReferenceNode expression) { + stringBuilder.append("r").append(expression.getColumn().getTable().getName()).append(".") + .append(expression.getColumn().getName()); + } + + @Override + protected void visit(ArangoDBConstant expression) { + stringBuilder.append(expression.getValue()); + } + + @Override + protected void visit(NewBinaryOperatorNode expression) { + stringBuilder.append("("); + visit(expression.getLeft()); + stringBuilder.append(" ").append(expression.getOperatorRepresentation()).append(" "); + visit(expression.getRight()); + stringBuilder.append(")"); + } + + @Override + protected void visit(NewUnaryPrefixOperatorNode expression) { + stringBuilder.append(expression.getOperatorRepresentation()).append("("); + visit(expression.getExpr()); + stringBuilder.append(")"); + } + + private void generateFrom(ArangoDBSelect expression) { + List forColumns = expression.getFromColumns(); + Set tables = new HashSet<>(); + for (ArangoDBSchema.ArangoDBColumn column : forColumns) { + tables.add(column.getTable()); + } + + for (ArangoDBSchema.ArangoDBTable table : tables) { + stringBuilder.append("FOR r").append(table.getName()).append(" IN ").append(table.getName()).append(" "); + } + } + + private void generateProject(ArangoDBSelect expression) { + List projectColumns = expression.getProjectionColumns(); + stringBuilder.append("RETURN {"); + String filler = ""; + for (ArangoDBSchema.ArangoDBColumn column : projectColumns) { + stringBuilder.append(filler); + filler = ", "; + stringBuilder.append(column.getTable().getName()).append("_").append(column.getName()).append(": r") + .append(column.getTable().getName()).append(".").append(column.getName()); + } + stringBuilder.append("}"); + } + + public ArangoDBSelectQuery getQuery() { + return new ArangoDBSelectQuery(stringBuilder.toString()); + } + +} diff --git a/src/sqlancer/arangodb/visitor/ArangoDBVisitor.java b/src/sqlancer/arangodb/visitor/ArangoDBVisitor.java new file mode 100644 index 000000000..322559952 --- /dev/null +++ b/src/sqlancer/arangodb/visitor/ArangoDBVisitor.java @@ -0,0 +1,47 @@ +package sqlancer.arangodb.visitor; + +import sqlancer.arangodb.ast.ArangoDBConstant; +import sqlancer.arangodb.ast.ArangoDBExpression; +import sqlancer.arangodb.ast.ArangoDBSelect; +import sqlancer.arangodb.query.ArangoDBSelectQuery; +import sqlancer.common.ast.newast.ColumnReferenceNode; +import sqlancer.common.ast.newast.NewBinaryOperatorNode; +import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; +import sqlancer.common.ast.newast.Node; + +public abstract class ArangoDBVisitor { + + protected abstract void visit(ArangoDBSelect expression); + + protected abstract void visit(ColumnReferenceNode expression); + + protected abstract void visit(ArangoDBConstant expression); + + protected abstract void visit(NewBinaryOperatorNode expression); + + protected abstract void visit(NewUnaryPrefixOperatorNode expression); + + public void visit(Node expressionNode) { + if (expressionNode instanceof ArangoDBSelect) { + visit((ArangoDBSelect) expressionNode); + } else if (expressionNode instanceof ColumnReferenceNode) { + visit((ColumnReferenceNode) expressionNode); + } else if (expressionNode instanceof ArangoDBConstant) { + visit((ArangoDBConstant) expressionNode); + } else if (expressionNode instanceof NewBinaryOperatorNode) { + visit((NewBinaryOperatorNode) expressionNode); + } else if (expressionNode instanceof NewUnaryPrefixOperatorNode) { + visit((NewUnaryPrefixOperatorNode) expressionNode); + } else { + System.out.println(expressionNode.getClass()); + System.exit(0); + throw new AssertionError(expressionNode); + } + } + + public static ArangoDBSelectQuery asSelectQuery(Node expressionNode) { + ArangoDBToQueryVisitor visitor = new ArangoDBToQueryVisitor(); + visitor.visit(expressionNode); + return visitor.getQuery(); + } +} From acf3d34ad4891cc7d1452734bdde79a07de36cf7 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 2 Mar 2021 17:00:12 +0100 Subject: [PATCH 18/24] Support adding indexes when inserting data for ArangoDB A new generator and query has been added to support the new functionality. --- src/sqlancer/arangodb/ArangoDBOptions.java | 3 ++ src/sqlancer/arangodb/ArangoDBProvider.java | 5 +- .../gen/ArangoDBCreateIndexGenerator.java | 21 ++++++++ .../query/ArangoDBCreateIndexQuery.java | 54 +++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/sqlancer/arangodb/gen/ArangoDBCreateIndexGenerator.java create mode 100644 src/sqlancer/arangodb/query/ArangoDBCreateIndexQuery.java diff --git a/src/sqlancer/arangodb/ArangoDBOptions.java b/src/sqlancer/arangodb/ArangoDBOptions.java index 04e1038f9..cdb7ee759 100644 --- a/src/sqlancer/arangodb/ArangoDBOptions.java +++ b/src/sqlancer/arangodb/ArangoDBOptions.java @@ -22,6 +22,9 @@ public class ArangoDBOptions implements DBMSSpecificOptions getTestOracleFactory() { return oracles; diff --git a/src/sqlancer/arangodb/ArangoDBProvider.java b/src/sqlancer/arangodb/ArangoDBProvider.java index 28160107d..fbf6f153e 100644 --- a/src/sqlancer/arangodb/ArangoDBProvider.java +++ b/src/sqlancer/arangodb/ArangoDBProvider.java @@ -13,6 +13,7 @@ import sqlancer.ProviderAdapter; import sqlancer.Randomly; import sqlancer.StatementExecutor; +import sqlancer.arangodb.gen.ArangoDBCreateIndexGenerator; import sqlancer.arangodb.gen.ArangoDBInsertGenerator; import sqlancer.arangodb.gen.ArangoDBTableGenerator; import sqlancer.common.log.LoggableFactory; @@ -26,7 +27,7 @@ public ArangoDBProvider() { } enum Action implements AbstractAction { - INSERT(ArangoDBInsertGenerator::getQuery); + INSERT(ArangoDBInsertGenerator::getQuery), CREATE_INDEX(ArangoDBCreateIndexGenerator::getQuery); private final ArangoDBQueryProvider queryProvider; @@ -45,6 +46,8 @@ private static int mapActions(ArangoDBGlobalState globalState, Action a) { switch (a) { case INSERT: return r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); + case CREATE_INDEX: + return r.getInteger(0, globalState.getDmbsSpecificOptions().maxNumberIndexes); default: throw new AssertionError(a); } diff --git a/src/sqlancer/arangodb/gen/ArangoDBCreateIndexGenerator.java b/src/sqlancer/arangodb/gen/ArangoDBCreateIndexGenerator.java new file mode 100644 index 000000000..cc66a3ef9 --- /dev/null +++ b/src/sqlancer/arangodb/gen/ArangoDBCreateIndexGenerator.java @@ -0,0 +1,21 @@ +package sqlancer.arangodb.gen; + +import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.ArangoDBQueryAdapter; +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.arangodb.query.ArangoDBCreateIndexQuery; + +public final class ArangoDBCreateIndexGenerator { + private ArangoDBCreateIndexGenerator() { + + } + + public static ArangoDBQueryAdapter getQuery(ArangoDBProvider.ArangoDBGlobalState globalState) { + ArangoDBSchema.ArangoDBColumn column = null; + while (column == null) { + ArangoDBSchema.ArangoDBTable randomTable = globalState.getSchema().getRandomTable(); + column = randomTable.getRandomColumn(); + } + return new ArangoDBCreateIndexQuery(column); + } +} diff --git a/src/sqlancer/arangodb/query/ArangoDBCreateIndexQuery.java b/src/sqlancer/arangodb/query/ArangoDBCreateIndexQuery.java new file mode 100644 index 000000000..6c2cc1b75 --- /dev/null +++ b/src/sqlancer/arangodb/query/ArangoDBCreateIndexQuery.java @@ -0,0 +1,54 @@ +package sqlancer.arangodb.query; + +import java.util.Collections; + +import com.arangodb.ArangoCollection; + +import sqlancer.GlobalState; +import sqlancer.Main; +import sqlancer.arangodb.ArangoDBConnection; +import sqlancer.arangodb.ArangoDBQueryAdapter; +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.common.query.ExpectedErrors; + +public class ArangoDBCreateIndexQuery extends ArangoDBQueryAdapter { + + private final ArangoDBSchema.ArangoDBColumn column; + + public ArangoDBCreateIndexQuery(ArangoDBSchema.ArangoDBColumn column) { + this.column = column; + } + + @Override + public boolean couldAffectSchema() { + return false; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + try { + ArangoCollection collection = globalState.getConnection().getDatabase() + .collection(column.getTable().getName()); + collection.ensureHashIndex(Collections.singletonList(column.getName()), null); + Main.nrSuccessfulActions.addAndGet(1); + return true; + } catch (Exception e) { + Main.nrUnsuccessfulActions.addAndGet(1); + throw e; + } + } + + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } + + @Override + public String getLogString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("db.").append(column.getTable().getName()) + .append(".ensureIndex({type: \"hash\", fields: [ \"").append(column.getName()).append("\" ]});"); + return stringBuilder.toString(); + } +} From 242c2458f6732c907145887f0bb7aee1fd2a4a02 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 2 Mar 2021 22:14:44 +0100 Subject: [PATCH 19/24] Add support for computed functions for ArangoDB Similar to MongoDB, in ArangoDB variables for computed values are created and with a special keyword LET calculated. --- src/sqlancer/arangodb/ast/ArangoDBSelect.java | 20 +++++ .../ArangoDBComputedExpressionGenerator.java | 85 +++++++++++++++++++ .../test/ArangoDBQueryPartitioningBase.java | 14 +++ .../visitor/ArangoDBToQueryVisitor.java | 43 +++++++++- .../arangodb/visitor/ArangoDBVisitor.java | 30 ++++--- 5 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 src/sqlancer/arangodb/gen/ArangoDBComputedExpressionGenerator.java diff --git a/src/sqlancer/arangodb/ast/ArangoDBSelect.java b/src/sqlancer/arangodb/ast/ArangoDBSelect.java index cbba174c1..9fb91d553 100644 --- a/src/sqlancer/arangodb/ast/ArangoDBSelect.java +++ b/src/sqlancer/arangodb/ast/ArangoDBSelect.java @@ -10,6 +10,8 @@ public class ArangoDBSelect implements Node { private List projectionColumns; private boolean hasFilter; private Node filterClause; + private boolean hasComputed; + private List> computedClause; public List getFromColumns() { if (fromColumns == null || fromColumns.isEmpty()) { @@ -56,4 +58,22 @@ public Node getFilterClause() { public boolean hasFilter() { return hasFilter; } + + public void setComputedClause(List> computedColumns) { + if (computedColumns == null || computedColumns.isEmpty()) { + hasComputed = false; + this.computedClause = null; + return; + } + hasComputed = true; + this.computedClause = computedColumns; + } + + public List> getComputedClause() { + return computedClause; + } + + public boolean hasComputed() { + return hasComputed; + } } diff --git a/src/sqlancer/arangodb/gen/ArangoDBComputedExpressionGenerator.java b/src/sqlancer/arangodb/gen/ArangoDBComputedExpressionGenerator.java new file mode 100644 index 000000000..8a3b98871 --- /dev/null +++ b/src/sqlancer/arangodb/gen/ArangoDBComputedExpressionGenerator.java @@ -0,0 +1,85 @@ +package sqlancer.arangodb.gen; + +import sqlancer.Randomly; +import sqlancer.arangodb.ArangoDBProvider; +import sqlancer.arangodb.ArangoDBSchema; +import sqlancer.arangodb.ast.ArangoDBConstant; +import sqlancer.arangodb.ast.ArangoDBExpression; +import sqlancer.common.ast.newast.ColumnReferenceNode; +import sqlancer.common.ast.newast.NewFunctionNode; +import sqlancer.common.ast.newast.Node; +import sqlancer.common.gen.UntypedExpressionGenerator; + +public class ArangoDBComputedExpressionGenerator + extends UntypedExpressionGenerator, ArangoDBSchema.ArangoDBColumn> { + private final ArangoDBProvider.ArangoDBGlobalState globalState; + + public ArangoDBComputedExpressionGenerator(ArangoDBProvider.ArangoDBGlobalState globalState) { + this.globalState = globalState; + } + + @Override + public Node generateConstant() { + ArangoDBSchema.ArangoDBDataType dataType = ArangoDBSchema.ArangoDBDataType.getRandom(); + switch (dataType) { + case INTEGER: + return ArangoDBConstant.createIntegerConstant((int) globalState.getRandomly().getInteger()); + case BOOLEAN: + return ArangoDBConstant.createBooleanConstant(Randomly.getBoolean()); + case DOUBLE: + return ArangoDBConstant.createDoubleConstant(globalState.getRandomly().getDouble()); + case STRING: + return ArangoDBConstant.createStringConstant(globalState.getRandomly().getString()); + default: + throw new AssertionError(dataType); + } + } + + public enum ComputedFunction { + ADD(2, "+"), MINUS(2, "-"), MULTIPLY(2, "*"), DIVISION(2, "/"), MODULUS(2, "%"); + + private final int nrArgs; + private final String operatorName; + + ComputedFunction(int nrArgs, String operatorName) { + this.nrArgs = nrArgs; + this.operatorName = operatorName; + } + + public static ComputedFunction getRandom() { + return Randomly.fromOptions(values()); + } + + public int getNrArgs() { + return nrArgs; + } + + public String getOperatorName() { + return operatorName; + } + } + + @Override + protected Node generateExpression(int depth) { + if (depth >= globalState.getOptions().getMaxExpressionDepth() || Randomly.getBoolean()) { + return generateLeafNode(); + } + ComputedFunction function = ComputedFunction.getRandom(); + return new NewFunctionNode<>(generateExpressions(depth + 1, function.getNrArgs()), function); + } + + @Override + protected Node generateColumn() { + return new ColumnReferenceNode<>(Randomly.fromList(columns)); + } + + @Override + public Node negatePredicate(Node predicate) { + throw new UnsupportedOperationException(); + } + + @Override + public Node isNull(Node expr) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java index 97c5cc154..e260e01f3 100644 --- a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java +++ b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java @@ -1,5 +1,6 @@ package sqlancer.arangodb.test; +import java.util.ArrayList; import java.util.List; import sqlancer.Randomly; @@ -7,6 +8,7 @@ import sqlancer.arangodb.ArangoDBSchema; import sqlancer.arangodb.ast.ArangoDBExpression; import sqlancer.arangodb.ast.ArangoDBSelect; +import sqlancer.arangodb.gen.ArangoDBComputedExpressionGenerator; import sqlancer.arangodb.gen.ArangoDBFilterExpressionGenerator; import sqlancer.common.ast.newast.Node; import sqlancer.common.gen.ExpressionGenerator; @@ -40,6 +42,18 @@ public void check() throws Exception { select = new ArangoDBSelect<>(); select.setFromColumns(targetColumns); select.setProjectionColumns(Randomly.nonEmptySubset(targetColumns)); + generateComputedClause(); + } + + private void generateComputedClause() { + List> computedColumns = new ArrayList<>(); + int numberComputedColumns = state.getRandomly().getInteger(0, 4); + ArangoDBComputedExpressionGenerator generator = new ArangoDBComputedExpressionGenerator(state); + generator.setColumns(targetColumns); + for (int i = 0; i < numberComputedColumns; i++) { + computedColumns.add(generator.generateExpression()); + } + select.setComputedClause(computedColumns); } private void generateTargetColumns() { diff --git a/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java b/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java index 160249df8..05683623c 100644 --- a/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java +++ b/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java @@ -8,12 +8,15 @@ import sqlancer.arangodb.ast.ArangoDBConstant; import sqlancer.arangodb.ast.ArangoDBExpression; import sqlancer.arangodb.ast.ArangoDBSelect; +import sqlancer.arangodb.gen.ArangoDBComputedExpressionGenerator; import sqlancer.arangodb.query.ArangoDBSelectQuery; import sqlancer.common.ast.newast.ColumnReferenceNode; import sqlancer.common.ast.newast.NewBinaryOperatorNode; +import sqlancer.common.ast.newast.NewFunctionNode; import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; +import sqlancer.common.ast.newast.Node; -public class ArangoDBToQueryVisitor extends ArangoDBVisitor { +public class ArangoDBToQueryVisitor extends ArangoDBVisitor { private final StringBuilder stringBuilder; @@ -24,17 +27,34 @@ public ArangoDBToQueryVisitor() { @Override protected void visit(ArangoDBSelect expression) { generateFrom(expression); + generateComputed(expression); + generateFilter(expression); + generateProject(expression); + } + + private void generateFilter(ArangoDBSelect expression) { if (expression.hasFilter()) { stringBuilder.append("FILTER "); visit(expression.getFilterClause()); stringBuilder.append(" "); } - generateProject(expression); + } + private void generateComputed(ArangoDBSelect expression) { + if (expression.hasComputed()) { + List> computedClause = expression.getComputedClause(); + int computedNumber = 0; + for (Node computedExpression : computedClause) { + stringBuilder.append("LET c").append(computedNumber).append(" = "); + visit(computedExpression); + stringBuilder.append(" "); + computedNumber++; + } + } } @Override - protected void visit(ColumnReferenceNode expression) { + protected void visit(ColumnReferenceNode expression) { stringBuilder.append("r").append(expression.getColumn().getTable().getName()).append(".") .append(expression.getColumn().getName()); } @@ -60,6 +80,23 @@ protected void visit(NewUnaryPrefixOperatorNode expression) stringBuilder.append(")"); } + @Override + protected void visit(NewFunctionNode expression) { + if (!(expression.getFunc() instanceof ArangoDBComputedExpressionGenerator.ComputedFunction)) { + throw new UnsupportedOperationException(); + } + ArangoDBComputedExpressionGenerator.ComputedFunction function = (ArangoDBComputedExpressionGenerator.ComputedFunction) expression + .getFunc(); + if (function.getNrArgs() != 2) { + throw new UnsupportedOperationException(); + } + stringBuilder.append("("); + visit(expression.getArgs().get(0)); + stringBuilder.append(" ").append(function.getOperatorName()).append(" "); + visit(expression.getArgs().get(1)); + stringBuilder.append(")"); + } + private void generateFrom(ArangoDBSelect expression) { List forColumns = expression.getFromColumns(); Set tables = new HashSet<>(); diff --git a/src/sqlancer/arangodb/visitor/ArangoDBVisitor.java b/src/sqlancer/arangodb/visitor/ArangoDBVisitor.java index 322559952..f1db84cf5 100644 --- a/src/sqlancer/arangodb/visitor/ArangoDBVisitor.java +++ b/src/sqlancer/arangodb/visitor/ArangoDBVisitor.java @@ -6,35 +6,39 @@ import sqlancer.arangodb.query.ArangoDBSelectQuery; import sqlancer.common.ast.newast.ColumnReferenceNode; import sqlancer.common.ast.newast.NewBinaryOperatorNode; +import sqlancer.common.ast.newast.NewFunctionNode; import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; import sqlancer.common.ast.newast.Node; -public abstract class ArangoDBVisitor { +public abstract class ArangoDBVisitor { - protected abstract void visit(ArangoDBSelect expression); + protected abstract void visit(ArangoDBSelect expression); - protected abstract void visit(ColumnReferenceNode expression); + protected abstract void visit(ColumnReferenceNode expression); protected abstract void visit(ArangoDBConstant expression); - protected abstract void visit(NewBinaryOperatorNode expression); + protected abstract void visit(NewBinaryOperatorNode expression); - protected abstract void visit(NewUnaryPrefixOperatorNode expression); + protected abstract void visit(NewUnaryPrefixOperatorNode expression); - public void visit(Node expressionNode) { + protected abstract void visit(NewFunctionNode expression); + + @SuppressWarnings("unchecked") + public void visit(Node expressionNode) { if (expressionNode instanceof ArangoDBSelect) { - visit((ArangoDBSelect) expressionNode); - } else if (expressionNode instanceof ColumnReferenceNode) { - visit((ColumnReferenceNode) expressionNode); + visit((ArangoDBSelect) expressionNode); + } else if (expressionNode instanceof ColumnReferenceNode) { + visit((ColumnReferenceNode) expressionNode); } else if (expressionNode instanceof ArangoDBConstant) { visit((ArangoDBConstant) expressionNode); } else if (expressionNode instanceof NewBinaryOperatorNode) { - visit((NewBinaryOperatorNode) expressionNode); + visit((NewBinaryOperatorNode) expressionNode); } else if (expressionNode instanceof NewUnaryPrefixOperatorNode) { - visit((NewUnaryPrefixOperatorNode) expressionNode); + visit((NewUnaryPrefixOperatorNode) expressionNode); + } else if (expressionNode instanceof NewFunctionNode) { + visit((NewFunctionNode) expressionNode); } else { - System.out.println(expressionNode.getClass()); - System.exit(0); throw new AssertionError(expressionNode); } } From 6de066ef8ad516219ebc5c53ffaa1000acf20d2e Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 2 Mar 2021 22:42:52 +0100 Subject: [PATCH 20/24] Enable computation values in filter expressions for ArangoDB --- .../gen/ArangoDBFilterExpressionGenerator.java | 18 ++++++++++++++++-- .../test/ArangoDBQueryPartitioningBase.java | 4 +++- .../visitor/ArangoDBToQueryVisitor.java | 8 ++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/sqlancer/arangodb/gen/ArangoDBFilterExpressionGenerator.java b/src/sqlancer/arangodb/gen/ArangoDBFilterExpressionGenerator.java index 902a33c68..1a2fc4b5e 100644 --- a/src/sqlancer/arangodb/gen/ArangoDBFilterExpressionGenerator.java +++ b/src/sqlancer/arangodb/gen/ArangoDBFilterExpressionGenerator.java @@ -21,6 +21,7 @@ public class ArangoDBFilterExpressionGenerator extends UntypedExpressionGenerator, ArangoDBSchema.ArangoDBColumn> { private final ArangoDBProvider.ArangoDBGlobalState globalState; + private int numberOfComputedVariables; private enum Expression { BINARY_LOGICAL, UNARY_PREFIX, BINARY_COMPARISON @@ -30,6 +31,10 @@ public ArangoDBFilterExpressionGenerator(ArangoDBProvider.ArangoDBGlobalState gl this.globalState = globalState; } + public void setNumberOfComputedVariables(int numberOfComputedVariables) { + this.numberOfComputedVariables = numberOfComputedVariables; + } + @Override public Node generateConstant() { ArangoDBSchema.ArangoDBDataType dataType = ArangoDBSchema.ArangoDBDataType.getRandom(); @@ -71,8 +76,17 @@ protected Node generateExpression(int depth) { @Override protected Node generateColumn() { - ArangoDBSchema.ArangoDBColumn column = Randomly.fromList(columns); - return new ColumnReferenceNode<>(column); + ArangoDBSchema.ArangoDBTable dummy = new ArangoDBSchema.ArangoDBTable("", new ArrayList<>(), false); + if (Randomly.getBoolean() || numberOfComputedVariables == 0) { + ArangoDBSchema.ArangoDBColumn column = Randomly.fromList(columns); + return new ColumnReferenceNode<>(column); + } else { + int maxNumber = globalState.getRandomly().getInteger(0, numberOfComputedVariables); + ArangoDBSchema.ArangoDBColumn column = new ArangoDBSchema.ArangoDBColumn("c" + maxNumber, + ArangoDBSchema.ArangoDBDataType.INTEGER, false, false); + column.setTable(dummy); + return new ColumnReferenceNode<>(column); + } } @Override diff --git a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java index e260e01f3..f583ed04f 100644 --- a/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java +++ b/src/sqlancer/arangodb/test/ArangoDBQueryPartitioningBase.java @@ -23,6 +23,7 @@ public class ArangoDBQueryPartitioningBase protected List targetColumns; protected ArangoDBFilterExpressionGenerator expressionGenerator; protected ArangoDBSelect select; + protected int numberComputedColumns; protected ArangoDBQueryPartitioningBase(ArangoDBProvider.ArangoDBGlobalState state) { super(state); @@ -35,9 +36,11 @@ protected ExpressionGenerator> getGen() { @Override public void check() throws Exception { + numberComputedColumns = state.getRandomly().getInteger(0, 4); schema = state.getSchema(); generateTargetColumns(); expressionGenerator = new ArangoDBFilterExpressionGenerator(state).setColumns(targetColumns); + expressionGenerator.setNumberOfComputedVariables(numberComputedColumns); initializeTernaryPredicateVariants(); select = new ArangoDBSelect<>(); select.setFromColumns(targetColumns); @@ -47,7 +50,6 @@ public void check() throws Exception { private void generateComputedClause() { List> computedColumns = new ArrayList<>(); - int numberComputedColumns = state.getRandomly().getInteger(0, 4); ArangoDBComputedExpressionGenerator generator = new ArangoDBComputedExpressionGenerator(state); generator.setColumns(targetColumns); for (int i = 0; i < numberComputedColumns; i++) { diff --git a/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java b/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java index 05683623c..59f4ababa 100644 --- a/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java +++ b/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java @@ -55,8 +55,12 @@ private void generateComputed(ArangoDBSelect expression) { @Override protected void visit(ColumnReferenceNode expression) { - stringBuilder.append("r").append(expression.getColumn().getTable().getName()).append(".") - .append(expression.getColumn().getName()); + if (expression.getColumn().getTable().getName().equals("")) { + stringBuilder.append(expression.getColumn().getName()); + } else { + stringBuilder.append("r").append(expression.getColumn().getTable().getName()).append(".") + .append(expression.getColumn().getName()); + } } @Override From 97f5e0b83c435815b9364037ddf88af122aa566a Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 2 Mar 2021 23:52:29 +0100 Subject: [PATCH 21/24] Make query generation null safe with option in MongoDB --- src/sqlancer/mongodb/MongoDBOptions.java | 3 +++ src/sqlancer/mongodb/MongoDBProvider.java | 2 +- src/sqlancer/mongodb/MongoDBSchema.java | 14 ++++++----- .../MongoDBComputedExpressionGenerator.java | 2 +- .../mongodb/gen/MongoDBConstantGenerator.java | 12 ++++----- .../gen/MongoDBMatchExpressionGenerator.java | 8 +++--- .../mongodb/gen/MongoDBTableGenerator.java | 7 +++++- .../test/MongoDBQueryPartitioningBase.java | 25 ++++++++++--------- 8 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/sqlancer/mongodb/MongoDBOptions.java b/src/sqlancer/mongodb/MongoDBOptions.java index 5023a6fb8..4d894cdca 100644 --- a/src/sqlancer/mongodb/MongoDBOptions.java +++ b/src/sqlancer/mongodb/MongoDBOptions.java @@ -37,6 +37,9 @@ public class MongoDBOptions implements DBMSSpecificOptions oracles = Arrays.asList(QUERY_PARTITIONING); diff --git a/src/sqlancer/mongodb/MongoDBProvider.java b/src/sqlancer/mongodb/MongoDBProvider.java index cf4375d01..cac7cb4ce 100644 --- a/src/sqlancer/mongodb/MongoDBProvider.java +++ b/src/sqlancer/mongodb/MongoDBProvider.java @@ -88,7 +88,7 @@ public void generateDatabase(MongoDBGlobalState globalState) throws Exception { for (int i = 0; i < Randomly.fromOptions(4, 5, 6); i++) { boolean success; do { - MongoDBQueryAdapter query = new MongoDBTableGenerator().getQuery(globalState); + MongoDBQueryAdapter query = new MongoDBTableGenerator(globalState).getQuery(globalState); success = globalState.executeStatement(query); } while (!success); } diff --git a/src/sqlancer/mongodb/MongoDBSchema.java b/src/sqlancer/mongodb/MongoDBSchema.java index 535e0d5f8..4968ab9d1 100644 --- a/src/sqlancer/mongodb/MongoDBSchema.java +++ b/src/sqlancer/mongodb/MongoDBSchema.java @@ -55,14 +55,16 @@ public BsonType getBsonType() { } }; - public static MongoDBDataType getRandom() { - // TODO: If String is enabled, there are type issues. Find a way to have a cast or operation on top of the - // query - // TODO: to solve this issue. - MongoDBDataType[] valuesWithoutString = new MongoDBDataType[values().length - 1]; + public static MongoDBDataType getRandom(MongoDBGlobalState state) { + MongoDBDataType[] valuesWithoutString; + if (state.getDmbsSpecificOptions().nullSafety) { + valuesWithoutString = new MongoDBDataType[values().length - 1]; + } else { + valuesWithoutString = new MongoDBDataType[values().length]; + } int i = 0; for (MongoDBDataType type : values()) { - if (type.equals(STRING)) { + if (type.equals(STRING) && state.getDmbsSpecificOptions().nullSafety) { continue; } valuesWithoutString[i] = type; diff --git a/src/sqlancer/mongodb/gen/MongoDBComputedExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBComputedExpressionGenerator.java index 1090b3f2f..fd5959ce6 100644 --- a/src/sqlancer/mongodb/gen/MongoDBComputedExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBComputedExpressionGenerator.java @@ -67,7 +67,7 @@ public String getOperator() { @Override public Node generateConstant() { - MongoDBSchema.MongoDBDataType type = MongoDBSchema.MongoDBDataType.getRandom(); + MongoDBSchema.MongoDBDataType type = MongoDBSchema.MongoDBDataType.getRandom(globalState); MongoDBConstantGenerator generator = new MongoDBConstantGenerator(globalState); return generator.generateConstantWithType(type); } diff --git a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java index bda5c87e6..e81291543 100644 --- a/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBConstantGenerator.java @@ -42,7 +42,7 @@ public Node generateConstantWithType(MongoDBDataType option) } public void addRandomConstant(Document document, String key) { - MongoDBDataType type = MongoDBDataType.getRandom(); + MongoDBDataType type = MongoDBDataType.getRandom(globalState); addRandomConstantWithType(document, key, type); } @@ -67,12 +67,10 @@ public void addRandomConstantWithType(Document document, String key, MongoDBData constant = new MongoDBDoubleConstant(globalState.getRandomly().getDouble()); constant.setValueInDocument(document, key); return; - // TODO: If String is enabled, there are type issues. Find a way to have a cast or operation on top of the query - // TODO: to solve this issue. - // case STRING: - // constant = new MongoDBStringConstant(globalState.getRandomly().getString()); - // constant.setValueInDocument(document, key); - // return; + case STRING: + constant = new MongoDBConstant.MongoDBStringConstant(globalState.getRandomly().getString()); + constant.setValueInDocument(document, key); + return; case INTEGER: constant = new MongoDBIntegerConstant((int) globalState.getRandomly().getInteger()); constant.setValueInDocument(document, key); diff --git a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java index 6414ea185..191d0195e 100644 --- a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java @@ -87,7 +87,7 @@ protected Node generateExpression(int depth) { @Override public Node generateConstant() { - MongoDBDataType type = MongoDBDataType.getRandom(); + MongoDBDataType type = MongoDBDataType.getRandom(globalState); MongoDBConstantGenerator generator = new MongoDBConstantGenerator(globalState); if (Randomly.getBooleanWithSmallProbability()) { return MongoDBConstant.createNullConstant(); @@ -97,9 +97,9 @@ public Node generateConstant() { public Node generateConstant(MongoDBDataType type) { MongoDBConstantGenerator generator = new MongoDBConstantGenerator(globalState); - // if (Randomly.getBooleanWithSmallProbability()) { - // return MongoDBConstant.createNullConstant(); - // } + if (Randomly.getBooleanWithSmallProbability() && !globalState.getDmbsSpecificOptions().nullSafety) { + return MongoDBConstant.createNullConstant(); + } return generator.generateConstantWithType(type); } diff --git a/src/sqlancer/mongodb/gen/MongoDBTableGenerator.java b/src/sqlancer/mongodb/gen/MongoDBTableGenerator.java index f71ea6411..6a4f33d38 100644 --- a/src/sqlancer/mongodb/gen/MongoDBTableGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBTableGenerator.java @@ -15,6 +15,11 @@ public class MongoDBTableGenerator { private MongoDBTable table; private final List columnsToBeAdded = new ArrayList<>(); + private final MongoDBGlobalState state; + + public MongoDBTableGenerator(MongoDBGlobalState state) { + this.state = state; + } public MongoDBQueryAdapter getQuery(MongoDBGlobalState globalState) { String tableName = globalState.getSchema().getFreeTableName(); @@ -32,7 +37,7 @@ public MongoDBQueryAdapter getQuery(MongoDBGlobalState globalState) { } private MongoDBDataType createColumn(String columnName) { - MongoDBDataType columnType = MongoDBDataType.getRandom(); + MongoDBDataType columnType = MongoDBDataType.getRandom(state); MongoDBColumn newColumn = new MongoDBColumn(columnName, columnType, false, false); newColumn.setTable(table); columnsToBeAdded.add(newColumn); diff --git a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java index 6421482a0..b26aa6028 100644 --- a/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java +++ b/src/sqlancer/mongodb/test/MongoDBQueryPartitioningBase.java @@ -42,12 +42,11 @@ public void check() throws Exception { initializeTernaryPredicateVariants(); select = new MongoDBSelect<>(mainTable.getName(), targetColumns.get(0)); select.setProjectionList(targetColumns); - // TODO: Find a new way to split the query such that it can handle null fields to activate lookups - // if (Randomly.getBooleanWithRatherLowProbability()) { - select.setLookupList(targetColumns); - /* - * } else { select.setLookupList(Randomly.nonEmptySubset(targetColumns)); } - */ + if (Randomly.getBooleanWithRatherLowProbability()) { + select.setLookupList(targetColumns); + } else { + select.setLookupList(Randomly.nonEmptySubset(targetColumns)); + } if (state.getDmbsSpecificOptions().testComputedValues) { generateComputedColumns(); } @@ -70,12 +69,14 @@ private void generateTargetColumns() { targetColumns.add(new MongoDBColumnTestReference(c, true)); } List joinsOtherTables = new ArrayList<>(); - // TODO: Find a new way to split the query such that it can handle null fields to activate lookups - /* - * for (int i = 1; i < targetTables.getTables().size(); i++) { MongoDBTable procTable = - * targetTables.getTables().get(i); for (MongoDBColumn c : procTable.getColumns()) { joinsOtherTables.add(new - * MongoDBColumnTestReference(c, false)); } } - */ + if (!state.getDmbsSpecificOptions().nullSafety) { + for (int i = 1; i < targetTables.getTables().size(); i++) { + MongoDBTable procTable = targetTables.getTables().get(i); + for (MongoDBColumn c : procTable.getColumns()) { + joinsOtherTables.add(new MongoDBColumnTestReference(c, false)); + } + } + } if (!joinsOtherTables.isEmpty()) { int randNumber = state.getRandomly().getInteger(1, Math.min(joinsOtherTables.size(), 4)); List subsetJoinsOtherTables = Randomly.nonEmptySubset(joinsOtherTables, From 8e7ef813f423c6a2a5b66bdc9a4cdfd272f21d40 Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 3 Mar 2021 13:02:39 +0100 Subject: [PATCH 22/24] Introduce remove/reduce oracle to SQLancer for MongoDB This oracle generates a random query, executes it and if the result set is not empty, chooses a document at random and removes it from the collection. The query is executed again to check if the document is really removed and at the end a new document is generated and inserted to make sure that the dataset is not decreasing in size. --- .../common/oracle/RemoveReduceOracleBase.java | 29 ++++++ src/sqlancer/mongodb/MongoDBOptions.java | 12 ++- .../mongodb/query/MongoDBRemoveQuery.java | 59 ++++++++++++ .../mongodb/test/MongoDBRemoveReduceBase.java | 89 +++++++++++++++++++ .../test/MongoDBRemoveReduceTester.java | 49 ++++++++++ 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 src/sqlancer/common/oracle/RemoveReduceOracleBase.java create mode 100644 src/sqlancer/mongodb/query/MongoDBRemoveQuery.java create mode 100644 src/sqlancer/mongodb/test/MongoDBRemoveReduceBase.java create mode 100644 src/sqlancer/mongodb/test/MongoDBRemoveReduceTester.java diff --git a/src/sqlancer/common/oracle/RemoveReduceOracleBase.java b/src/sqlancer/common/oracle/RemoveReduceOracleBase.java new file mode 100644 index 000000000..c177f072e --- /dev/null +++ b/src/sqlancer/common/oracle/RemoveReduceOracleBase.java @@ -0,0 +1,29 @@ +package sqlancer.common.oracle; + +import sqlancer.GlobalState; +import sqlancer.common.gen.ExpressionGenerator; + +public abstract class RemoveReduceOracleBase> implements TestOracle { + + protected E predicate; + + protected final S state; + + protected RemoveReduceOracleBase(S state) { + this.state = state; + } + + protected void initializeRemoveReduceOracle() { + 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/mongodb/MongoDBOptions.java b/src/sqlancer/mongodb/MongoDBOptions.java index 4d894cdca..cf632085b 100644 --- a/src/sqlancer/mongodb/MongoDBOptions.java +++ b/src/sqlancer/mongodb/MongoDBOptions.java @@ -1,6 +1,7 @@ package sqlancer.mongodb; import static sqlancer.mongodb.MongoDBOptions.MongoDBOracleFactory.QUERY_PARTITIONING; +import static sqlancer.mongodb.MongoDBOptions.MongoDBOracleFactory.REMOVE_REDUCE; import java.util.ArrayList; import java.util.Arrays; @@ -13,6 +14,7 @@ import sqlancer.common.oracle.CompositeTestOracle; import sqlancer.common.oracle.TestOracle; import sqlancer.mongodb.test.MongoDBQueryPartitioningWhereTester; +import sqlancer.mongodb.test.MongoDBRemoveReduceTester; public class MongoDBOptions implements DBMSSpecificOptions { @@ -41,7 +43,7 @@ public class MongoDBOptions implements DBMSSpecificOptions oracles = Arrays.asList(QUERY_PARTITIONING); + public List oracles = Arrays.asList(QUERY_PARTITIONING, REMOVE_REDUCE); @Override public List getTestOracleFactory() { @@ -56,6 +58,14 @@ public TestOracle create(MongoDBProvider.MongoDBGlobalState globalState) throws oracles.add(new MongoDBQueryPartitioningWhereTester(globalState)); return new CompositeTestOracle(oracles, globalState); } + }, + REMOVE_REDUCE { + @Override + public TestOracle create(MongoDBProvider.MongoDBGlobalState globalState) throws Exception { + List oracles = new ArrayList<>(); + oracles.add(new MongoDBRemoveReduceTester(globalState)); + return new CompositeTestOracle(oracles, globalState); + } } } } diff --git a/src/sqlancer/mongodb/query/MongoDBRemoveQuery.java b/src/sqlancer/mongodb/query/MongoDBRemoveQuery.java new file mode 100644 index 000000000..6fe1c9e3f --- /dev/null +++ b/src/sqlancer/mongodb/query/MongoDBRemoveQuery.java @@ -0,0 +1,59 @@ +package sqlancer.mongodb.query; + +import org.bson.Document; +import org.bson.types.ObjectId; + +import com.mongodb.client.result.DeleteResult; + +import sqlancer.GlobalState; +import sqlancer.Main; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.mongodb.MongoDBConnection; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.MongoDBSchema; + +public class MongoDBRemoveQuery extends MongoDBQueryAdapter { + + private final String objectId; + private final MongoDBSchema.MongoDBTable table; + + public MongoDBRemoveQuery(MongoDBSchema.MongoDBTable table, String objectId) { + this.objectId = objectId; + this.table = table; + } + + @Override + public boolean couldAffectSchema() { + return true; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + try { + DeleteResult result = globalState.getConnection().getDatabase().getCollection(table.getName()) + .deleteOne(new Document("_id", new ObjectId(objectId))); + if (result.wasAcknowledged()) { + Main.nrSuccessfulActions.addAndGet(1); + } else { + Main.nrUnsuccessfulActions.addAndGet(1); + } + return result.wasAcknowledged(); + } catch (Exception e) { + Main.nrUnsuccessfulActions.addAndGet(1); + return false; + } + } + + @Override + public ExpectedErrors getExpectedErrors() { + return new ExpectedErrors(); + } + + @Override + public String getLogString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("db.").append(table.getName()).append(".remove({'_id': '").append(objectId).append("'})"); + return stringBuilder.toString(); + } +} diff --git a/src/sqlancer/mongodb/test/MongoDBRemoveReduceBase.java b/src/sqlancer/mongodb/test/MongoDBRemoveReduceBase.java new file mode 100644 index 000000000..2a2a54744 --- /dev/null +++ b/src/sqlancer/mongodb/test/MongoDBRemoveReduceBase.java @@ -0,0 +1,89 @@ +package sqlancer.mongodb.test; + +import java.util.ArrayList; +import java.util.List; + +import sqlancer.Randomly; +import sqlancer.common.ast.newast.Node; +import sqlancer.common.gen.ExpressionGenerator; +import sqlancer.common.oracle.RemoveReduceOracleBase; +import sqlancer.common.oracle.TestOracle; +import sqlancer.mongodb.MongoDBProvider; +import sqlancer.mongodb.MongoDBSchema; +import sqlancer.mongodb.ast.MongoDBExpression; +import sqlancer.mongodb.ast.MongoDBSelect; +import sqlancer.mongodb.gen.MongoDBComputedExpressionGenerator; +import sqlancer.mongodb.gen.MongoDBMatchExpressionGenerator; + +public class MongoDBRemoveReduceBase extends + RemoveReduceOracleBase, MongoDBProvider.MongoDBGlobalState> implements TestOracle { + + protected MongoDBSchema schema; + protected MongoDBSchema.MongoDBTables targetTables; + protected MongoDBSchema.MongoDBTable mainTable; + protected List targetColumns; + protected MongoDBMatchExpressionGenerator expressionGenerator; + protected MongoDBSelect select; + + protected MongoDBRemoveReduceBase(MongoDBProvider.MongoDBGlobalState state) { + super(state); + } + + @Override + public void check() throws Exception { + schema = state.getSchema(); + targetTables = schema.getRandomTableNonEmptyTables(); + mainTable = targetTables.getTables().get(0); + generateTargetColumns(); + expressionGenerator = new MongoDBMatchExpressionGenerator(state).setColumns(targetColumns); + initializeRemoveReduceOracle(); + select = new MongoDBSelect<>(mainTable.getName(), targetColumns.get(0)); + select.setProjectionList(targetColumns); + if (Randomly.getBooleanWithRatherLowProbability()) { + select.setLookupList(targetColumns); + } else { + select.setLookupList(Randomly.nonEmptySubset(targetColumns)); + } + if (state.getDmbsSpecificOptions().testComputedValues) { + generateComputedColumns(); + } + } + + private void generateTargetColumns() { + targetColumns = new ArrayList<>(); + for (MongoDBSchema.MongoDBColumn c : mainTable.getColumns()) { + targetColumns.add(new MongoDBColumnTestReference(c, true)); + } + List joinsOtherTables = new ArrayList<>(); + if (!state.getDmbsSpecificOptions().nullSafety) { + for (int i = 1; i < targetTables.getTables().size(); i++) { + MongoDBSchema.MongoDBTable procTable = targetTables.getTables().get(i); + for (MongoDBSchema.MongoDBColumn c : procTable.getColumns()) { + joinsOtherTables.add(new MongoDBColumnTestReference(c, false)); + } + } + } + if (!joinsOtherTables.isEmpty()) { + int randNumber = state.getRandomly().getInteger(1, Math.min(joinsOtherTables.size(), 4)); + List subsetJoinsOtherTables = Randomly.nonEmptySubset(joinsOtherTables, + randNumber); + targetColumns.addAll(subsetJoinsOtherTables); + } + } + + private void generateComputedColumns() { + List> computedColumns = new ArrayList<>(); + int numberComputedColumns = state.getRandomly().getInteger(1, 4); + MongoDBComputedExpressionGenerator generator = new MongoDBComputedExpressionGenerator(state) + .setColumns(targetColumns); + for (int i = 0; i < numberComputedColumns; i++) { + computedColumns.add(generator.generateExpression()); + } + select.setComputedClause(computedColumns); + } + + @Override + protected ExpressionGenerator> getGen() { + return expressionGenerator; + } +} diff --git a/src/sqlancer/mongodb/test/MongoDBRemoveReduceTester.java b/src/sqlancer/mongodb/test/MongoDBRemoveReduceTester.java new file mode 100644 index 000000000..829f63f23 --- /dev/null +++ b/src/sqlancer/mongodb/test/MongoDBRemoveReduceTester.java @@ -0,0 +1,49 @@ +package sqlancer.mongodb.test; + +import static sqlancer.mongodb.MongoDBComparatorHelper.getResultSetAsDocumentList; + +import java.util.List; + +import org.bson.Document; + +import sqlancer.Randomly; +import sqlancer.mongodb.MongoDBProvider; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.gen.MongoDBInsertGenerator; +import sqlancer.mongodb.query.MongoDBRemoveQuery; +import sqlancer.mongodb.query.MongoDBSelectQuery; + +public class MongoDBRemoveReduceTester extends MongoDBRemoveReduceBase { + public MongoDBRemoveReduceTester(MongoDBProvider.MongoDBGlobalState state) { + super(state); + } + + @Override + public void check() throws Exception { + super.check(); + + select.setWithCountClause(false); + + select.setFilterClause(predicate); + MongoDBSelectQuery selectQuery = new MongoDBSelectQuery(select); + List firstResultSet = getResultSetAsDocumentList(selectQuery, state); + if (firstResultSet == null || firstResultSet.isEmpty()) { + return; + } + + Document documentToRemove = Randomly.fromList(firstResultSet); + MongoDBRemoveQuery removeQuery = new MongoDBRemoveQuery(mainTable, documentToRemove.get("_id").toString()); + state.executeStatement(removeQuery); + + selectQuery = new MongoDBSelectQuery(select); + List secondResultSet = getResultSetAsDocumentList(selectQuery, state); + + MongoDBQueryAdapter insertQuery = MongoDBInsertGenerator.getQuery(state); + state.executeStatement(insertQuery); + + if (secondResultSet.size() + 1 != firstResultSet.size()) { + String assertMessage = "The Result Sizes mismatches!"; + throw new AssertionError(assertMessage); + } + } +} From b2afeefacff2274378682ad2f575a3524e43477f Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 22 Mar 2021 23:00:04 +0100 Subject: [PATCH 23/24] Add Cosmos as a database configuration When using cosmos make sure to set the configuration string in CosmosProvider. --- src/sqlancer/Main.java | 2 + src/sqlancer/cosmos/CosmosProvider.java | 74 +++++++++++++++++++++++ src/sqlancer/mongodb/MongoDBProvider.java | 4 +- 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 src/sqlancer/cosmos/CosmosProvider.java diff --git a/src/sqlancer/Main.java b/src/sqlancer/Main.java index bfc0cabd8..c5da9ec98 100644 --- a/src/sqlancer/Main.java +++ b/src/sqlancer/Main.java @@ -28,6 +28,7 @@ import sqlancer.common.log.Loggable; import sqlancer.common.query.Query; import sqlancer.common.query.SQLancerResultSet; +import sqlancer.cosmos.CosmosProvider; import sqlancer.duckdb.DuckDBProvider; import sqlancer.h2.H2Provider; import sqlancer.mariadb.MariaDBProvider; @@ -557,6 +558,7 @@ private boolean run(MainOptions options, ExecutorService execService, providers.add(new DuckDBProvider()); providers.add(new H2Provider()); providers.add(new MongoDBProvider()); + providers.add(new CosmosProvider()); providers.add(new ArangoDBProvider()); return providers; } diff --git a/src/sqlancer/cosmos/CosmosProvider.java b/src/sqlancer/cosmos/CosmosProvider.java new file mode 100644 index 000000000..a8681d3af --- /dev/null +++ b/src/sqlancer/cosmos/CosmosProvider.java @@ -0,0 +1,74 @@ +package sqlancer.cosmos; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; + +import sqlancer.IgnoreMeException; +import sqlancer.ProviderAdapter; +import sqlancer.Randomly; +import sqlancer.StatementExecutor; +import sqlancer.common.log.LoggableFactory; +import sqlancer.mongodb.MongoDBConnection; +import sqlancer.mongodb.MongoDBLoggableFactory; +import sqlancer.mongodb.MongoDBOptions; +import sqlancer.mongodb.MongoDBQueryAdapter; +import sqlancer.mongodb.gen.MongoDBTableGenerator; + +public class CosmosProvider extends + ProviderAdapter { + + public CosmosProvider() { + super(sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState.class, MongoDBOptions.class); + } + + @Override + public void generateDatabase(sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState globalState) throws Exception { + for (int i = 0; i < Randomly.fromOptions(4, 5, 6); i++) { + boolean success; + do { + MongoDBQueryAdapter query = new MongoDBTableGenerator(globalState).getQuery(globalState); + success = globalState.executeStatement(query); + } while (!success); + } + StatementExecutor se = new StatementExecutor<>( + globalState, sqlancer.mongodb.MongoDBProvider.Action.values(), + sqlancer.mongodb.MongoDBProvider::mapActions, (q) -> { + if (globalState.getSchema().getDatabaseTables().isEmpty()) { + throw new IgnoreMeException(); + } + }); + se.executeStatements(); + } + + @Override + public MongoDBConnection createDatabase(sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState globalState) + throws Exception { + String connectionString = ""; + if (connectionString.equals("")) { + throw new AssertionError("Please set connection string for cosmos database, located in CosmosProvider"); + } + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(connectionString)).build(); + MongoClient mongoClient = MongoClients.create(settings); + MongoDatabase database = mongoClient.getDatabase(globalState.getDatabaseName()); + database.drop(); + return new MongoDBConnection(mongoClient, database); + } + + @Override + public String getDBMSName() { + return "cosmos"; + } + + @Override + public LoggableFactory getLoggableFactory() { + return new MongoDBLoggableFactory(); + } + + @Override + protected void checkViewsAreValid(sqlancer.mongodb.MongoDBProvider.MongoDBGlobalState globalState) { + } +} diff --git a/src/sqlancer/mongodb/MongoDBProvider.java b/src/sqlancer/mongodb/MongoDBProvider.java index cac7cb4ce..4712b5d9d 100644 --- a/src/sqlancer/mongodb/MongoDBProvider.java +++ b/src/sqlancer/mongodb/MongoDBProvider.java @@ -28,7 +28,7 @@ public MongoDBProvider() { super(MongoDBGlobalState.class, MongoDBOptions.class); } - enum Action implements AbstractAction { + public enum Action implements AbstractAction { INSERT(MongoDBInsertGenerator::getQuery), CREATE_INDEX(MongoDBIndexGenerator::getQuery); private final MongoDBQueryProvider queryProvider; @@ -43,7 +43,7 @@ public Query getQuery(MongoDBGlobalState globalState) throws } } - private static int mapActions(MongoDBGlobalState globalState, Action a) { + public static int mapActions(MongoDBGlobalState globalState, Action a) { Randomly r = globalState.getRandomly(); switch (a) { case INSERT: From e0b272dc818c48d353a3d8cba6fa70947114f919 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 28 Mar 2021 17:06:06 +0200 Subject: [PATCH 24/24] Clean up and improve code for pull request See Pull request #319 on github.com/sqlancer for more information. --- .../arangodb/ArangoDBComparatorHelper.java | 2 +- src/sqlancer/arangodb/ArangoDBProvider.java | 5 +- .../arangodb/ArangoDBQueryAdapter.java | 1 + .../gen/ArangoDBCreateIndexGenerator.java | 7 +- .../visitor/ArangoDBToQueryVisitor.java | 1 + .../mongodb/MongoDBComparatorHelper.java | 2 +- src/sqlancer/mongodb/MongoDBConnection.java | 6 +- src/sqlancer/mongodb/MongoDBProvider.java | 2 +- src/sqlancer/mongodb/MongoDBSchema.java | 74 +++++-------------- .../gen/MongoDBMatchExpressionGenerator.java | 2 +- 10 files changed, 37 insertions(+), 65 deletions(-) diff --git a/src/sqlancer/arangodb/ArangoDBComparatorHelper.java b/src/sqlancer/arangodb/ArangoDBComparatorHelper.java index 70cc451fe..2a00a312d 100644 --- a/src/sqlancer/arangodb/ArangoDBComparatorHelper.java +++ b/src/sqlancer/arangodb/ArangoDBComparatorHelper.java @@ -27,10 +27,10 @@ public static List getResultSetAsDocumentList(ArangoDBSelectQuery result = query.getResultSet(); return result; } catch (Exception e) { - Main.nrUnsuccessfulActions.addAndGet(1); if (e instanceof IgnoreMeException) { throw e; } + Main.nrUnsuccessfulActions.addAndGet(1); if (e.getMessage() == null) { throw new AssertionError(query.getLogString(), e); } diff --git a/src/sqlancer/arangodb/ArangoDBProvider.java b/src/sqlancer/arangodb/ArangoDBProvider.java index fbf6f153e..d63d4a1cc 100644 --- a/src/sqlancer/arangodb/ArangoDBProvider.java +++ b/src/sqlancer/arangodb/ArangoDBProvider.java @@ -57,7 +57,7 @@ public static class ArangoDBGlobalState extends GlobalState schemaTables = new ArrayList<>(); - public synchronized void addTable(ArangoDBSchema.ArangoDBTable table) { + public void addTable(ArangoDBSchema.ArangoDBTable table) { schemaTables.add(table); } @@ -111,6 +111,9 @@ public ArangoDBConnection createDatabase(ArangoDBGlobalState globalState) throws ArangoDatabase database = arangoDB.db(globalState.getDatabaseName()); try { database.drop(); + // When the database does not exist, an ArangoDB exception is thrown. Since we are not sure + // if this is the first time the database is used, the simplest is dropping it and ignoring + // the exception. } catch (Exception ignored) { } diff --git a/src/sqlancer/arangodb/ArangoDBQueryAdapter.java b/src/sqlancer/arangodb/ArangoDBQueryAdapter.java index 2d1b77af0..34cdb3709 100644 --- a/src/sqlancer/arangodb/ArangoDBQueryAdapter.java +++ b/src/sqlancer/arangodb/ArangoDBQueryAdapter.java @@ -5,6 +5,7 @@ public abstract class ArangoDBQueryAdapter extends Query { @Override public String getQueryString() { + // Should not be called as it is used only in SQL dependent classes throw new UnsupportedOperationException(); } diff --git a/src/sqlancer/arangodb/gen/ArangoDBCreateIndexGenerator.java b/src/sqlancer/arangodb/gen/ArangoDBCreateIndexGenerator.java index cc66a3ef9..6a1b872da 100644 --- a/src/sqlancer/arangodb/gen/ArangoDBCreateIndexGenerator.java +++ b/src/sqlancer/arangodb/gen/ArangoDBCreateIndexGenerator.java @@ -11,11 +11,8 @@ private ArangoDBCreateIndexGenerator() { } public static ArangoDBQueryAdapter getQuery(ArangoDBProvider.ArangoDBGlobalState globalState) { - ArangoDBSchema.ArangoDBColumn column = null; - while (column == null) { - ArangoDBSchema.ArangoDBTable randomTable = globalState.getSchema().getRandomTable(); - column = randomTable.getRandomColumn(); - } + ArangoDBSchema.ArangoDBTable randomTable = globalState.getSchema().getRandomTable(); + ArangoDBSchema.ArangoDBColumn column = randomTable.getRandomColumn(); return new ArangoDBCreateIndexQuery(column); } } diff --git a/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java b/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java index 59f4ababa..f82995d5e 100644 --- a/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java +++ b/src/sqlancer/arangodb/visitor/ArangoDBToQueryVisitor.java @@ -91,6 +91,7 @@ protected void visit(NewFunctionNode expression) { } ArangoDBComputedExpressionGenerator.ComputedFunction function = (ArangoDBComputedExpressionGenerator.ComputedFunction) expression .getFunc(); + // TODO: Support functions with a different number of arguments. if (function.getNrArgs() != 2) { throw new UnsupportedOperationException(); } diff --git a/src/sqlancer/mongodb/MongoDBComparatorHelper.java b/src/sqlancer/mongodb/MongoDBComparatorHelper.java index b03e24422..49b692645 100644 --- a/src/sqlancer/mongodb/MongoDBComparatorHelper.java +++ b/src/sqlancer/mongodb/MongoDBComparatorHelper.java @@ -27,10 +27,10 @@ public static List getResultSetAsDocumentList(MongoDBSelectQuery adapt result = adapter.getResultSet(); return result; } catch (Exception e) { - Main.nrUnsuccessfulActions.addAndGet(1); if (e instanceof IgnoreMeException) { throw e; } + Main.nrUnsuccessfulActions.addAndGet(1); if (e.getMessage() == null) { throw new AssertionError(adapter.getLogString(), e); } diff --git a/src/sqlancer/mongodb/MongoDBConnection.java b/src/sqlancer/mongodb/MongoDBConnection.java index c15a46be5..6971bd79c 100644 --- a/src/sqlancer/mongodb/MongoDBConnection.java +++ b/src/sqlancer/mongodb/MongoDBConnection.java @@ -1,5 +1,8 @@ package sqlancer.mongodb; +import org.bson.BsonDocument; +import org.bson.BsonString; + import com.mongodb.client.MongoClient; import com.mongodb.client.MongoDatabase; @@ -17,7 +20,8 @@ public MongoDBConnection(MongoClient client, MongoDatabase database) { @Override public String getDatabaseVersion() throws Exception { - return "4.4, Java API 4.1"; + return client.getDatabase("dbname").runCommand(new BsonDocument("buildinfo", new BsonString(""))).get("version") + .toString(); } @Override diff --git a/src/sqlancer/mongodb/MongoDBProvider.java b/src/sqlancer/mongodb/MongoDBProvider.java index 4712b5d9d..5ff549d32 100644 --- a/src/sqlancer/mongodb/MongoDBProvider.java +++ b/src/sqlancer/mongodb/MongoDBProvider.java @@ -59,7 +59,7 @@ public static class MongoDBGlobalState extends GlobalState schemaTables = new ArrayList<>(); - public synchronized void addTable(MongoDBTable table) { + public void addTable(MongoDBTable table) { schemaTables.add(table); } diff --git a/src/sqlancer/mongodb/MongoDBSchema.java b/src/sqlancer/mongodb/MongoDBSchema.java index 4968ab9d1..e9a0afb99 100644 --- a/src/sqlancer/mongodb/MongoDBSchema.java +++ b/src/sqlancer/mongodb/MongoDBSchema.java @@ -1,7 +1,10 @@ package sqlancer.mongodb; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.bson.BsonType; @@ -17,67 +20,30 @@ public class MongoDBSchema extends AbstractSchema { - public enum MongoDBDataType implements HasBsonType { - INTEGER { - @Override - public BsonType getBsonType() { - return BsonType.INT32; - } - }, - STRING { - @Override - public BsonType getBsonType() { - return BsonType.STRING; - } - }, - BOOLEAN { - @Override - public BsonType getBsonType() { - return BsonType.BOOLEAN; - } - }, - DOUBLE { - @Override - public BsonType getBsonType() { - return BsonType.DOUBLE; - } - }, - DATE_TIME { - @Override - public BsonType getBsonType() { - return BsonType.DATE_TIME; - } - }, - TIMESTAMP { - @Override - public BsonType getBsonType() { - return BsonType.TIMESTAMP; - } - }; + public enum MongoDBDataType { + INTEGER(BsonType.INT32), STRING(BsonType.STRING), BOOLEAN(BsonType.BOOLEAN), DOUBLE(BsonType.DOUBLE), + DATE_TIME(BsonType.DATE_TIME), TIMESTAMP(BsonType.TIMESTAMP); + + private final BsonType bsonType; + + MongoDBDataType(BsonType type) { + this.bsonType = type; + } + + public BsonType getBsonType() { + return bsonType; + } public static MongoDBDataType getRandom(MongoDBGlobalState state) { - MongoDBDataType[] valuesWithoutString; + Set valueSet = new HashSet<>(Arrays.asList(values())); if (state.getDmbsSpecificOptions().nullSafety) { - valuesWithoutString = new MongoDBDataType[values().length - 1]; - } else { - valuesWithoutString = new MongoDBDataType[values().length]; + valueSet.remove(STRING); } - int i = 0; - for (MongoDBDataType type : values()) { - if (type.equals(STRING) && state.getDmbsSpecificOptions().nullSafety) { - continue; - } - valuesWithoutString[i] = type; - i++; - } - return Randomly.fromOptions(valuesWithoutString); + MongoDBDataType[] configuredValues = new MongoDBDataType[valueSet.size()]; + return Randomly.fromOptions(valueSet.toArray(configuredValues)); } } - public interface HasBsonType { - BsonType getBsonType(); - } - public static class MongoDBColumn extends AbstractTableColumn { private final boolean isId; diff --git a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java index 191d0195e..0645d7f24 100644 --- a/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java +++ b/src/sqlancer/mongodb/gen/MongoDBMatchExpressionGenerator.java @@ -105,7 +105,7 @@ public Node generateConstant(MongoDBDataType type) { private String getRandomizedRegexOptions() { List s = Randomly.subset("i", "m", "x", "s"); - return s.stream().reduce("", (current, newVal) -> current + newVal); + return String.join("", s); } @Override