|
| 1 | +package sqlancer.cockroachdb.oracle; |
| 2 | + |
| 3 | +import java.io.IOException; |
| 4 | +import java.sql.SQLException; |
| 5 | +import java.util.ArrayList; |
| 6 | +import java.util.List; |
| 7 | +import java.util.stream.Collectors; |
| 8 | + |
| 9 | +import sqlancer.IgnoreMeException; |
| 10 | +import sqlancer.Randomly; |
| 11 | +import sqlancer.SQLGlobalState; |
| 12 | +import sqlancer.cockroachdb.CockroachDBCommon; |
| 13 | +import sqlancer.cockroachdb.CockroachDBErrors; |
| 14 | +import sqlancer.cockroachdb.CockroachDBProvider.CockroachDBGlobalState; |
| 15 | +import sqlancer.cockroachdb.CockroachDBSchema.CockroachDBColumn; |
| 16 | +import sqlancer.cockroachdb.CockroachDBSchema.CockroachDBDataType; |
| 17 | +import sqlancer.cockroachdb.CockroachDBSchema.CockroachDBTables; |
| 18 | +import sqlancer.cockroachdb.CockroachDBVisitor; |
| 19 | +import sqlancer.cockroachdb.ast.CockroachDBBinaryLogicalOperation; |
| 20 | +import sqlancer.cockroachdb.ast.CockroachDBBinaryLogicalOperation.CockroachDBBinaryLogicalOperator; |
| 21 | +import sqlancer.cockroachdb.ast.CockroachDBColumnReference; |
| 22 | +import sqlancer.cockroachdb.ast.CockroachDBExpression; |
| 23 | +import sqlancer.cockroachdb.ast.CockroachDBJoin; |
| 24 | +import sqlancer.cockroachdb.ast.CockroachDBJoin.JoinType; |
| 25 | +import sqlancer.cockroachdb.ast.CockroachDBSelect; |
| 26 | +import sqlancer.cockroachdb.ast.CockroachDBTableReference; |
| 27 | +import sqlancer.cockroachdb.gen.CockroachDBExpressionGenerator; |
| 28 | +import sqlancer.common.DBMSCommon; |
| 29 | +import sqlancer.common.oracle.CERTOracleBase; |
| 30 | +import sqlancer.common.oracle.TestOracle; |
| 31 | +import sqlancer.common.query.SQLQueryAdapter; |
| 32 | +import sqlancer.common.query.SQLancerResultSet; |
| 33 | + |
| 34 | +public class CockroachDBCERTOracle extends CERTOracleBase<CockroachDBGlobalState> |
| 35 | + implements TestOracle<CockroachDBGlobalState> { |
| 36 | + private CockroachDBExpressionGenerator gen; |
| 37 | + private CockroachDBSelect select; |
| 38 | + |
| 39 | + public CockroachDBCERTOracle(CockroachDBGlobalState globalState) { |
| 40 | + super(globalState); |
| 41 | + CockroachDBErrors.addExpressionErrors(errors); |
| 42 | + } |
| 43 | + |
| 44 | + @Override |
| 45 | + public void check() throws SQLException { |
| 46 | + queryPlan1Sequences = new ArrayList<>(); |
| 47 | + queryPlan2Sequences = new ArrayList<>(); |
| 48 | + |
| 49 | + // Randomly generate a query |
| 50 | + CockroachDBTables tables = state.getSchema().getRandomTableNonEmptyTables(2); |
| 51 | + List<CockroachDBExpression> tableList = CockroachDBCommon.getTableReferences( |
| 52 | + tables.getTables().stream().map(t -> new CockroachDBTableReference(t)).collect(Collectors.toList())); |
| 53 | + gen = new CockroachDBExpressionGenerator(state).setColumns(tables.getColumns()); |
| 54 | + List<CockroachDBExpression> fetchColumns = new ArrayList<>(); |
| 55 | + fetchColumns.addAll(Randomly.nonEmptySubset(tables.getColumns()).stream() |
| 56 | + .map(c -> new CockroachDBColumnReference(c)).collect(Collectors.toList())); |
| 57 | + select = new CockroachDBSelect(); |
| 58 | + select.setFetchColumns(fetchColumns); |
| 59 | + select.setFromList(tableList); |
| 60 | + select.setDistinct(Randomly.getBoolean()); |
| 61 | + if (Randomly.getBoolean()) { |
| 62 | + select.setWhereClause(gen.generateExpression(CockroachDBDataType.BOOL.get())); |
| 63 | + } |
| 64 | + if (Randomly.getBoolean()) { |
| 65 | + select.setGroupByExpressions(fetchColumns); |
| 66 | + if (Randomly.getBoolean()) { |
| 67 | + select.setHavingClause(gen.generateExpression(CockroachDBDataType.BOOL.get())); |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + // Set the join. |
| 72 | + List<CockroachDBExpression> joinExpressions = getJoins(tableList, state); |
| 73 | + select.setJoinList(joinExpressions); |
| 74 | + |
| 75 | + // Get the result of the first query |
| 76 | + String queryString1 = CockroachDBVisitor.asString(select); |
| 77 | + int rowCount1 = getRow(state, queryString1, queryPlan1Sequences); |
| 78 | + |
| 79 | + // Mutate the query |
| 80 | + // Disable limit due to its false positive |
| 81 | + boolean increase = mutate(); |
| 82 | + |
| 83 | + // Get the result of the second query |
| 84 | + String queryString2 = CockroachDBVisitor.asString(select); |
| 85 | + int rowCount2 = getRow(state, queryString2, queryPlan2Sequences); |
| 86 | + |
| 87 | + // Check structural equivalence |
| 88 | + if (DBMSCommon.editDistance(queryPlan1Sequences, queryPlan2Sequences) > 1) { |
| 89 | + return; |
| 90 | + } |
| 91 | + |
| 92 | + // Check the results |
| 93 | + if (increase && rowCount1 > rowCount2 || !increase && rowCount1 < rowCount2) { |
| 94 | + throw new AssertionError("Inconsistent result for query: EXPLAIN " + queryString1 + "; --" + rowCount1 |
| 95 | + + "\nEXPLAIN " + queryString2 + "; --" + rowCount2); |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + private List<CockroachDBExpression> getJoins(List<CockroachDBExpression> tableList, |
| 100 | + CockroachDBGlobalState globalState) throws AssertionError { |
| 101 | + List<CockroachDBExpression> joinExpressions = new ArrayList<>(); |
| 102 | + while (tableList.size() >= 2 && Randomly.getPercentage() < 0.8) { |
| 103 | + CockroachDBTableReference leftTable = (CockroachDBTableReference) tableList.remove(0); |
| 104 | + CockroachDBTableReference rightTable = (CockroachDBTableReference) tableList.remove(0); |
| 105 | + List<CockroachDBColumn> columns = new ArrayList<>(leftTable.getTable().getColumns()); |
| 106 | + columns.addAll(rightTable.getTable().getColumns()); |
| 107 | + CockroachDBExpressionGenerator joinGen = new CockroachDBExpressionGenerator(globalState) |
| 108 | + .setColumns(columns); |
| 109 | + joinExpressions.add(CockroachDBJoin.createJoin(leftTable, rightTable, |
| 110 | + CockroachDBJoin.JoinType.getRandomExcept(JoinType.NATURAL), |
| 111 | + joinGen.generateExpression(CockroachDBDataType.BOOL.get()))); |
| 112 | + } |
| 113 | + return joinExpressions; |
| 114 | + } |
| 115 | + |
| 116 | + @Override |
| 117 | + protected boolean mutateJoin() { |
| 118 | + if (select.getJoinList().isEmpty()) { |
| 119 | + return false; |
| 120 | + } |
| 121 | + |
| 122 | + CockroachDBJoin join = (CockroachDBJoin) Randomly.fromList(select.getJoinList()); |
| 123 | + |
| 124 | + // CROSS does not need ON Condition, while other joins do |
| 125 | + // To avoid Null pointer, generating a new new condition when mutating CROSS to other joins |
| 126 | + if (join.getJoinType() == JoinType.CROSS) { |
| 127 | + List<CockroachDBColumn> columns = new ArrayList<>(); |
| 128 | + columns.addAll(((CockroachDBTableReference) join.getLeftTable()).getTable().getColumns()); |
| 129 | + columns.addAll(((CockroachDBTableReference) join.getRightTable()).getTable().getColumns()); |
| 130 | + CockroachDBExpressionGenerator joinGen2 = new CockroachDBExpressionGenerator(state).setColumns(columns); |
| 131 | + join.setOnCondition(joinGen2.generateExpression(CockroachDBDataType.BOOL.get())); |
| 132 | + } |
| 133 | + |
| 134 | + JoinType newJoinType = CockroachDBJoin.JoinType.INNER; |
| 135 | + if (join.getJoinType() == JoinType.LEFT || join.getJoinType() == JoinType.RIGHT) { // No invarient relation |
| 136 | + // between LEFT and RIGHT |
| 137 | + // join |
| 138 | + newJoinType = CockroachDBJoin.JoinType.getRandomExcept(JoinType.NATURAL, JoinType.LEFT, JoinType.RIGHT); |
| 139 | + } else { |
| 140 | + newJoinType = CockroachDBJoin.JoinType.getRandomExcept(JoinType.NATURAL, join.getJoinType()); |
| 141 | + } |
| 142 | + assert newJoinType != JoinType.NATURAL; // Natural Join is not supported for CERT |
| 143 | + boolean increase = join.getJoinType().ordinal() < newJoinType.ordinal(); |
| 144 | + join.setJoinType(newJoinType); |
| 145 | + return increase; |
| 146 | + } |
| 147 | + |
| 148 | + @Override |
| 149 | + protected boolean mutateDistinct() { |
| 150 | + boolean increase = select.isDistinct(); |
| 151 | + select.setDistinct(!select.isDistinct()); |
| 152 | + return increase; |
| 153 | + } |
| 154 | + |
| 155 | + @Override |
| 156 | + protected boolean mutateWhere() { |
| 157 | + boolean increase = select.getWhereClause() != null; |
| 158 | + if (increase) { |
| 159 | + select.setWhereClause(null); |
| 160 | + } else { |
| 161 | + select.setWhereClause(gen.generateExpression(CockroachDBDataType.BOOL.get())); |
| 162 | + } |
| 163 | + return increase; |
| 164 | + } |
| 165 | + |
| 166 | + @Override |
| 167 | + protected boolean mutateGroupBy() { |
| 168 | + boolean increase = select.getGroupByExpressions().size() > 0; |
| 169 | + if (increase) { |
| 170 | + select.clearGroupByExpressions(); |
| 171 | + } else { |
| 172 | + select.setGroupByExpressions(select.getFetchColumns()); |
| 173 | + } |
| 174 | + return increase; |
| 175 | + } |
| 176 | + |
| 177 | + @Override |
| 178 | + protected boolean mutateHaving() { |
| 179 | + if (select.getGroupByExpressions().size() == 0) { |
| 180 | + select.setGroupByExpressions(select.getFetchColumns()); |
| 181 | + select.setHavingClause(gen.generateExpression(CockroachDBDataType.BOOL.get())); |
| 182 | + return false; |
| 183 | + } else { |
| 184 | + if (select.getHavingClause() == null) { |
| 185 | + select.setHavingClause(gen.generateExpression(CockroachDBDataType.BOOL.get())); |
| 186 | + return false; |
| 187 | + } else { |
| 188 | + select.setHavingClause(null); |
| 189 | + return true; |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + @Override |
| 195 | + protected boolean mutateAnd() { |
| 196 | + if (select.getWhereClause() == null) { |
| 197 | + select.setWhereClause(gen.generateExpression(CockroachDBDataType.BOOL.get())); |
| 198 | + } else { |
| 199 | + CockroachDBExpression newWhere = new CockroachDBBinaryLogicalOperation(select.getWhereClause(), |
| 200 | + gen.generateExpression(CockroachDBDataType.BOOL.get()), CockroachDBBinaryLogicalOperator.AND); |
| 201 | + select.setWhereClause(newWhere); |
| 202 | + } |
| 203 | + return false; |
| 204 | + } |
| 205 | + |
| 206 | + @Override |
| 207 | + protected boolean mutateOr() { |
| 208 | + if (select.getWhereClause() == null) { |
| 209 | + select.setWhereClause(gen.generateExpression(CockroachDBDataType.BOOL.get())); |
| 210 | + return false; |
| 211 | + } else { |
| 212 | + CockroachDBExpression newWhere = new CockroachDBBinaryLogicalOperation(select.getWhereClause(), |
| 213 | + gen.generateExpression(CockroachDBDataType.BOOL.get()), CockroachDBBinaryLogicalOperator.OR); |
| 214 | + select.setWhereClause(newWhere); |
| 215 | + return true; |
| 216 | + } |
| 217 | + } |
| 218 | + |
| 219 | + @Override |
| 220 | + protected boolean mutateLimit() { |
| 221 | + boolean increase = select.getLimitClause() != null; |
| 222 | + if (increase) { |
| 223 | + select.setLimitClause(null); |
| 224 | + } else { |
| 225 | + select.setLimitClause(gen.generateConstant(CockroachDBDataType.INT.get())); |
| 226 | + } |
| 227 | + return increase; |
| 228 | + } |
| 229 | + |
| 230 | + private int getRow(SQLGlobalState<?, ?> globalState, String selectStr, List<String> queryPlanSequences) |
| 231 | + throws AssertionError, SQLException { |
| 232 | + int row = -1; |
| 233 | + String explainQuery = "EXPLAIN (VERBOSE) " + selectStr; |
| 234 | + |
| 235 | + // Log the query |
| 236 | + if (globalState.getOptions().logEachSelect()) { |
| 237 | + globalState.getLogger().writeCurrent(explainQuery); |
| 238 | + try { |
| 239 | + globalState.getLogger().getCurrentFileWriter().flush(); |
| 240 | + } catch (IOException e) { |
| 241 | + e.printStackTrace(); |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + // Get the row count |
| 246 | + SQLQueryAdapter q = new SQLQueryAdapter(explainQuery, errors); |
| 247 | + try (SQLancerResultSet rs = q.executeAndGet(globalState)) { |
| 248 | + if (rs != null) { |
| 249 | + while (rs.next()) { |
| 250 | + String content = rs.getString(1); |
| 251 | + if (content.contains("count:")) { |
| 252 | + try { |
| 253 | + int number = Integer.parseInt(content.split("count: ")[1].split(" ")[0].replace(",", "")); |
| 254 | + if (row == -1) { |
| 255 | + row = number; |
| 256 | + } |
| 257 | + } catch (Exception e) { // To avoid the situation that no number is found |
| 258 | + } |
| 259 | + } |
| 260 | + if (content.contains("• ")) { |
| 261 | + String operation = content.split("• ")[1].split(" ")[0]; |
| 262 | + queryPlanSequences.add(operation); |
| 263 | + } |
| 264 | + } |
| 265 | + } |
| 266 | + } catch (Exception e) { |
| 267 | + throw new AssertionError(q.getQueryString(), e); |
| 268 | + } |
| 269 | + if (row == -1) { |
| 270 | + throw new IgnoreMeException(); |
| 271 | + } |
| 272 | + return row; |
| 273 | + } |
| 274 | + |
| 275 | +} |
0 commit comments