diff --git a/pom.xml b/pom.xml index 119ef6434..6ad79c6a9 100644 --- a/pom.xml +++ b/pom.xml @@ -329,8 +329,13 @@ org.slf4j - slf4j-simple - 2.0.6 + slf4j-api + 2.0.16 + + + org.slf4j + slf4j-simple + 2.0.16 ru.yandex.clickhouse @@ -378,6 +383,12 @@ flight-sql-jdbc-driver 16.1.0 + + junit + junit + 4.13.1 + compile + diff --git a/src/sqlancer/Main.java b/src/sqlancer/Main.java index 46841095b..18fc9ba04 100644 --- a/src/sqlancer/Main.java +++ b/src/sqlancer/Main.java @@ -33,6 +33,7 @@ import sqlancer.databend.DatabendProvider; import sqlancer.doris.DorisProvider; import sqlancer.duckdb.DuckDBProvider; +import sqlancer.feldera.FelderaProvider; import sqlancer.h2.H2Provider; import sqlancer.hsqldb.HSQLDBProvider; import sqlancer.mariadb.MariaDBProvider; @@ -734,6 +735,7 @@ private static void checkForIssue799(List> providers) providers.add(new DatabendProvider()); providers.add(new DorisProvider()); providers.add(new DuckDBProvider()); + providers.add(new FelderaProvider()); providers.add(new H2Provider()); providers.add(new HSQLDBProvider()); providers.add(new MariaDBProvider()); diff --git a/src/sqlancer/feldera/FelderaConnection.java b/src/sqlancer/feldera/FelderaConnection.java new file mode 100644 index 000000000..ddda794f9 --- /dev/null +++ b/src/sqlancer/feldera/FelderaConnection.java @@ -0,0 +1,90 @@ +package sqlancer.feldera; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import sqlancer.SQLancerDBConnection; +import sqlancer.feldera.client.FelderaClient; +import sqlancer.feldera.client.FelderaPipeline; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FelderaConnection implements SQLancerDBConnection { + private final FelderaClient client; + private final String pipelineName; + private final List inserts; + private boolean ready = false; + private String ddl; + + public FelderaConnection(String url, String pipelineName) { + this.client = new FelderaClient(url); + this.pipelineName = pipelineName; + this.inserts = new ArrayList<>(); + this.ddl = ""; + } + + public void prepare() throws Exception { + if (!ready) { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + + node.put("name", this.pipelineName); + node.put("description", "sqlancerTest"); + String ddlWithInserts = ddl + "--" + String.join("\n--", inserts); + node.put("program_code", ddlWithInserts); + node.putObject("runtime_config"); + node.putObject("program_config"); + + this.client.createPipeline(pipelineName, node.toString()); + this.client.start(this.pipelineName); + + for (String insert : this.inserts) { + this.client.exec(this.pipelineName, insert); + } + + ready = true; + } + } + + public String getPipelineName() { + return this.pipelineName; + } + + public FelderaClient getClient() { + return this.client; + } + + @Override + public String getDatabaseVersion() throws Exception { + int x = this.client.getPipeline(this.pipelineName).getVersion(); + return Integer.toString(x); + } + + @Override + public void close() throws Exception { + if (ready) { + this.client.shutdown(pipelineName); + } + } + + public FelderaPipeline get() throws Exception { + return this.client.getPipeline(pipelineName); + } + + public void buffer(String query) throws Exception { + if (query.startsWith("INSERT")) { + this.inserts.add(query); + } else { + this.ddl += query; + } + } + + public Map execute(String query) throws Exception { + return this.client.exec(this.pipelineName, query); + } + + public void shutdown() throws Exception { + this.client.shutdown(this.pipelineName); + } +} diff --git a/src/sqlancer/feldera/FelderaExpectedError.java b/src/sqlancer/feldera/FelderaExpectedError.java new file mode 100644 index 000000000..51b311ea4 --- /dev/null +++ b/src/sqlancer/feldera/FelderaExpectedError.java @@ -0,0 +1,23 @@ +package sqlancer.feldera; + +import sqlancer.common.query.ExpectedErrors; + +import java.util.ArrayList; +import java.util.List; + +public class FelderaExpectedError { + private FelderaExpectedError() { + } + + public static List getExpectedErrors() { + List errors = new ArrayList<>(); + errors.add("panic message:"); + return errors; + } + + public static ExpectedErrors expectedErrors() { + ExpectedErrors res = new ExpectedErrors(); + res.addAll(getExpectedErrors()); + return res; + } +} diff --git a/src/sqlancer/feldera/FelderaGlobalState.java b/src/sqlancer/feldera/FelderaGlobalState.java new file mode 100644 index 000000000..bf1c09d91 --- /dev/null +++ b/src/sqlancer/feldera/FelderaGlobalState.java @@ -0,0 +1,57 @@ +package sqlancer.feldera; + +import sqlancer.ExecutionTimer; +import sqlancer.GlobalState; +import sqlancer.common.query.Query; + +import java.util.ArrayList; +import java.util.List; + +public class FelderaGlobalState extends GlobalState { + List views = new ArrayList<>(); + + @Override + protected void executeEpilogue(Query q, boolean success, ExecutionTimer timer) throws Exception { + boolean logExecutionTime = getOptions().logExecutionTime(); + if (success && getOptions().printSucceedingStatements()) { + System.out.println(q.getQueryString()); + } + if (logExecutionTime) { + getLogger().writeCurrent(" -- " + timer.end().asString()); + } + if (q.couldAffectSchema()) { + updateSchema(); + } + } + + @Override + public void updateSchema() { + ; // do nothing + } + + @Override + public FelderaSchema getSchema() { + return super.getSchema(); + } + + @Override + protected FelderaSchema readSchema() throws Exception { + return FelderaSchema.fromConnection(getConnection()); + } + + public void addTable(FelderaSchema.FelderaTable table) { + FelderaSchema sch = getSchema(); + if (sch == null) { + sch = new FelderaSchema(getConnection().getPipelineName()); + } + setSchema(sch.addTable(table)); + } + + public void addView(String view) { + this.views.add(view); + } + + public List getViews() { + return this.views; + } +} diff --git a/src/sqlancer/feldera/FelderaLoggableFactory.java b/src/sqlancer/feldera/FelderaLoggableFactory.java new file mode 100644 index 000000000..1615acc71 --- /dev/null +++ b/src/sqlancer/feldera/FelderaLoggableFactory.java @@ -0,0 +1,54 @@ +package sqlancer.feldera; + +import sqlancer.common.log.Loggable; +import sqlancer.common.log.LoggableFactory; +import sqlancer.common.log.LoggedString; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.Query; +import sqlancer.feldera.query.FelderaOtherQuery; +import sqlancer.feldera.query.FelderaQueryAdapter; + +import java.io.PrintWriter; +import java.io.StringWriter; + +public class FelderaLoggableFactory extends LoggableFactory { + @Override + protected Loggable createLoggable(String input, String suffix) { + String completeString = input.trim(); + if (!input.endsWith(";")) { + completeString += ";"; + } + if (suffix != null && !suffix.isEmpty()) { + completeString += suffix; + } + return new LoggedString(completeString); + } + + @Override + public FelderaQueryAdapter getQueryForStateToReproduce(String queryString) { + return new FelderaOtherQuery(queryString, FelderaExpectedError.expectedErrors()); + } + + @Override + public FelderaQueryAdapter commentOutQuery(Query query) { + String queryString = query.getQueryString(); + String newQueryString = "-- " + queryString; + ExpectedErrors errors = new ExpectedErrors(); + return new FelderaOtherQuery(newQueryString, errors); + } + + @Override + protected Loggable infoToLoggable(String time, String pipelineName, String databaseVersion, long seedValue) { + String sb = "-- Time: " + time + "\n" + "-- Pipeline: " + pipelineName + "\n " + "-- Pipeline version: " + + databaseVersion + "\n" + "-- seed value: " + seedValue + "\n"; + return new LoggedString(sb); + } + + @Override + public Loggable convertStacktraceToLoggable(Throwable throwable) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + return new LoggedString("--" + sw.toString().replace("\n", "\n--")); + } +} diff --git a/src/sqlancer/feldera/FelderaOptions.java b/src/sqlancer/feldera/FelderaOptions.java new file mode 100644 index 000000000..195a358c1 --- /dev/null +++ b/src/sqlancer/feldera/FelderaOptions.java @@ -0,0 +1,26 @@ +package sqlancer.feldera; + +import java.util.List; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; + +import sqlancer.DBMSSpecificOptions; + +@Parameters(separators = "=", commandDescription = "Feldera (default " + FelderaOptions.DEFAULT_URL + ")") +public class FelderaOptions implements DBMSSpecificOptions { + + public static final String DEFAULT_URL = "http://127.0.0.1:8080"; + + @Parameter(names = "--oracle", description = "Specifies which test oracle should be used for Feldera") + public List oracle = List.of(FelderaOracleFactory.NOREC); + + @Parameter(names = "--connection-url", description = "Specifies the URL for connecting to the Feldera", arity = 1) + public String connection_url = DEFAULT_URL; + + @Override + public List getTestOracleFactory() { + return oracle; + } + +} diff --git a/src/sqlancer/feldera/FelderaOracleFactory.java b/src/sqlancer/feldera/FelderaOracleFactory.java new file mode 100644 index 000000000..da2113788 --- /dev/null +++ b/src/sqlancer/feldera/FelderaOracleFactory.java @@ -0,0 +1,14 @@ +package sqlancer.feldera; + +import sqlancer.OracleFactory; +import sqlancer.feldera.oracle.FelderaNoRECOracle; +import sqlancer.common.oracle.TestOracle; + +public enum FelderaOracleFactory implements OracleFactory { + NOREC { + @Override + public TestOracle create(FelderaGlobalState globalState) { + return new FelderaNoRECOracle(globalState); + } + }, +} \ No newline at end of file diff --git a/src/sqlancer/feldera/FelderaProvider.java b/src/sqlancer/feldera/FelderaProvider.java new file mode 100644 index 000000000..6451584d3 --- /dev/null +++ b/src/sqlancer/feldera/FelderaProvider.java @@ -0,0 +1,157 @@ +package sqlancer.feldera; + +import sqlancer.*; + +import com.google.auto.service.AutoService; +import sqlancer.common.log.LoggableFactory; + +import sqlancer.common.oracle.TestOracle; +import sqlancer.feldera.gen.FelderaInsertGenerator; +import sqlancer.feldera.gen.FelderaTableGenerator; +import sqlancer.feldera.gen.FelderaViewGenerator; +import sqlancer.feldera.query.FelderaOtherQuery; +import sqlancer.feldera.query.FelderaQueryProvider; + +import java.util.List; +import java.util.Objects; + +@AutoService(DatabaseProvider.class) +public class FelderaProvider extends ProviderAdapter { + + protected String url; + protected String pipelineName; + + public FelderaProvider() { + super(FelderaGlobalState.class, FelderaOptions.class); + } + + protected FelderaProvider(Class globalClass, Class optionClass) { + super(globalClass, optionClass); + } + + protected static int mapActions(FelderaGlobalState globalState, sqlancer.feldera.FelderaProvider.Action a) { + Randomly r = globalState.getRandomly(); + int nrPerformed; + if (Objects.requireNonNull(a) == sqlancer.feldera.FelderaProvider.Action.INSERT) { + nrPerformed = r.getInteger(0, globalState.getOptions().getMaxNumberInserts()); + } else { + throw new AssertionError(a); + } + return nrPerformed; + + } + + @Override + protected void checkViewsAreValid(FelderaGlobalState globalState) { + } + + @Override + public void generateDatabase(FelderaGlobalState globalState) throws Exception { + createTables(globalState, Randomly.fromOptions(4, 5, 6)); + createViews(globalState, Randomly.fromOptions(4, 5, 6)); + prepareTables(globalState); + } + + @Override + public FelderaConnection createDatabase(FelderaGlobalState globalState) throws Exception { + url = globalState.getDbmsSpecificOptions().connection_url; + pipelineName = globalState.getDatabaseName(); + + try (FelderaConnection connection = new FelderaConnection(url, pipelineName)) { + try { + connection.get(); + connection.shutdown(); + } catch (Exception ignored) { + } + + return connection; + } + } + + protected void createTables(FelderaGlobalState globalState, int numTables) throws Exception { + for (int i = 0; i < numTables; i++) { + String tableName = String.format("t%d", i); + FelderaTableGenerator generator = new FelderaTableGenerator(tableName); + FelderaOtherQuery createTable = generator.generate(); + FelderaSchema.FelderaTable table = generator.getTable(); + globalState.addTable(table); + globalState.executeStatement(createTable); + } + } + + protected void createViews(FelderaGlobalState globalState, int numViews) throws Exception { + for (int i = 0; i < numViews; i++) { + String viewName = String.format("v%d", i); + List views = FelderaViewGenerator.generate(globalState, viewName); + for (FelderaOtherQuery view : views) { + globalState.executeStatement(view); + globalState.addView(viewName); + } + } + } + + protected void prepareTables(FelderaGlobalState globalState) throws Exception { + StatementExecutor se = new StatementExecutor<>(globalState, + FelderaProvider.Action.values(), sqlancer.feldera.FelderaProvider::mapActions, (q) -> { + if (globalState.getSchema().getDatabaseTables().isEmpty()) { + throw new IgnoreMeException(); + } + }); + se.executeStatements(); + } + + @Override + public String getDBMSName() { + return "feldera"; + } + + @Override + public Reproducer generateAndTestDatabase(FelderaGlobalState globalState) throws Exception { + try { + generateDatabase(globalState); + globalState.getManager().incrementCreateDatabase(); + + TestOracle oracle = getTestOracle(globalState); + try (StateToReproduce.OracleRunReproductionState localState = globalState.getState().createLocalState()) { + assert localState != null; + try { + oracle.check(); + globalState.getManager().incrementSelectQueryCount(); + } catch (IgnoreMeException ignored) { + } catch (AssertionError e) { + Reproducer reproducer = oracle.getLastReproducer(); + if (reproducer != null) { + return reproducer; + } + throw e; + } + localState.executedWithoutError(); + } + } finally { + globalState.getConnection().close(); + } + return null; + } + + @Override + public LoggableFactory getLoggableFactory() { + return new FelderaLoggableFactory(); + } + + public enum Action implements AbstractAction { + INSERT(FelderaInsertGenerator::getQuery); + + private final FelderaQueryProvider sqlQueryProvider; + + Action(FelderaQueryProvider sqlQueryProvider) { + this.sqlQueryProvider = sqlQueryProvider; + } + + @Override + public FelderaOtherQuery getQuery(FelderaGlobalState state) throws Exception { + return new FelderaOtherQuery(sqlQueryProvider.getQuery(state).getQueryString(), + FelderaExpectedError.expectedErrors()); + } + } + +} diff --git a/src/sqlancer/feldera/FelderaSchema.java b/src/sqlancer/feldera/FelderaSchema.java new file mode 100644 index 000000000..f55641544 --- /dev/null +++ b/src/sqlancer/feldera/FelderaSchema.java @@ -0,0 +1,207 @@ +package sqlancer.feldera; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import sqlancer.Randomly; +import sqlancer.common.schema.*; +import sqlancer.feldera.ast.FelderaColumnReference; +import sqlancer.feldera.ast.FelderaConstant; +import sqlancer.feldera.ast.FelderaExpression; + +public class FelderaSchema extends AbstractSchema { + + private final String pipelineName; + + public FelderaSchema(List databaseTables, String pipelineName) { + super(databaseTables); + this.pipelineName = pipelineName; + } + + public FelderaSchema(String pipelineName) { + super(new ArrayList<>()); + this.pipelineName = pipelineName; + } + + public FelderaSchema addTable(FelderaTable table) { + List tables = new ArrayList<>(this.getDatabaseTables()); + tables.add(table); + + return new FelderaSchema(tables, this.pipelineName); + } + + public static FelderaDataType getColumnType(String typeString) { + switch (typeString.toUpperCase()) { + case "BOOLEAN": + return FelderaDataType.BOOLEAN; + case "TINYINT": + return FelderaDataType.TINYINT; + case "SMALLINT": + return FelderaDataType.SMALLINT; + case "INT": + return FelderaDataType.INT; + case "BIGINT": + return FelderaDataType.BIGINT; + case "VARCHAR": + return FelderaDataType.VARCHAR; + case "CHAR": + return FelderaDataType.CHAR; + case "NULL": + return FelderaDataType.NULL; + case "TIME": + return FelderaDataType.TIME; + case "DATE": + return FelderaDataType.DATE; + case "TIMESTAMP": + return FelderaDataType.TIMESTAMP; + case "REAL": + return FelderaDataType.REAL; + case "DOUBLE": + return FelderaDataType.DOUBLE; + default: + throw new AssertionError(typeString); + } + } + + public static FelderaSchema fromConnection(FelderaConnection con) throws Exception { + return new FelderaSchema(new ArrayList<>(), con.getPipelineName()); + } + + protected List getTableColumns(String tableName) throws Exception { + return this.getDatabaseTable(tableName).getColumns(); + } + + public FelderaTables getRandomTableNonEmptyTables() { + return new FelderaTables(Randomly.nonEmptySubset(getDatabaseTables())); + } + + public String getPipelineName() { + return pipelineName; + } + + public enum FelderaDataType { + BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, VARCHAR, CHAR, NULL, TIME, DATE, TIMESTAMP, + // DECIMAL, + // VARBINARY, + // INTERVAL, + // GEOMETRY, + // ROW, + // ARRAY, + // MAP, + // VARIANT, + REAL, DOUBLE; + + public static FelderaDataType getRandomNumericType() { + return Randomly + .fromList(Arrays.stream(values()).filter(FelderaDataType::isNumeric).collect(Collectors.toList())); + } + + public boolean isNumeric() { + switch (this) { + case REAL: + case DOUBLE: + case TINYINT: + case SMALLINT: + case INT: + case BIGINT: + return true; + default: + return false; + } + } + + public static FelderaDataType getRandomNonNullType() { + return Randomly.fromList( + Arrays.stream(values()).filter(t -> t != FelderaDataType.NULL).collect(Collectors.toList())); + } + + public static FelderaDataType getRandomType() { + return Randomly.fromOptions(values()); + } + + public FelderaExpression getRandomConstant(FelderaGlobalState globalState) { + if (Randomly.getBooleanWithSmallProbability()) { + return FelderaConstant.createNullConstant(); + } + + return FelderaConstant.getRandomConstant(globalState, this); + } + } + + public static class FelderaFieldColumn extends FelderaColumn { + public FelderaFieldColumn(String name, FelderaDataType columnType) { + super(name, columnType); + } + + public FelderaFieldColumn(String name, FelderaDataType columnType, boolean isNullable) { + super(name, columnType, isNullable); + // Note to self: later, assert that the Field column isn't something like INTERVAL + } + } + + public static class FelderaColumn extends AbstractTableColumn { + + private final boolean isNullable; + + public FelderaColumn(String name, FelderaDataType columnType) { + super(name, null, columnType); + this.isNullable = false; + } + + public FelderaColumn(String name, FelderaDataType columnType, boolean isNullable) { + super(name, null, columnType); + this.isNullable = isNullable; + } + + public FelderaColumnReference asColumnReference() { + return new FelderaColumnReference(this); + } + + public static FelderaColumn createDummy(String name) { + return new FelderaColumn(name, FelderaDataType.getRandomType()); + } + + public boolean isNullable() { + return isNullable; + } + } + + public static class FelderaTables extends AbstractTables { + + public FelderaTables(List tables) { + super(tables); + } + } + + public static class FelderaTable extends AbstractTable { + + public FelderaTable(String tableName, List columns) { + super(tableName, columns, null, false); + } + + // SELECT COUNT(*) FROM table; + @Override + public long getNrRows(FelderaGlobalState globalState) { + // TODO + return 0; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof FelderaTable) { + FelderaTable other = (FelderaTable) obj; + return Objects.equals(this.name, other.name) && this.getColumns() == other.getColumns(); + } else { + return false; + } + } + + public static List getAllColumns(List tables) { + return tables.stream().map(AbstractTable::getColumns).flatMap(List::stream).collect(Collectors.toList()); + } + } + +} diff --git a/src/sqlancer/feldera/FelderaToStringVisitor.java b/src/sqlancer/feldera/FelderaToStringVisitor.java new file mode 100644 index 000000000..349272c2b --- /dev/null +++ b/src/sqlancer/feldera/FelderaToStringVisitor.java @@ -0,0 +1,130 @@ +package sqlancer.feldera; + +import sqlancer.common.ast.newast.NewToStringVisitor; +import sqlancer.common.ast.newast.TableReferenceNode; +import sqlancer.feldera.ast.*; + +public class FelderaToStringVisitor extends NewToStringVisitor { + @Override + public void visitSpecific(FelderaExpression expr) { + if (expr.isBlackbox()) { + sb.append("blackbox("); + } + if (expr instanceof FelderaConstant) { + visit((FelderaConstant) expr); + } else if (expr instanceof FelderaSelect) { + visit((FelderaSelect) expr); + } else if (expr instanceof FelderaJoin) { + visit((FelderaJoin) expr); + } else if (expr instanceof FelderaCast) { + visit((FelderaCast) expr); + } else if (expr instanceof FelderaBetweenOperation) { + visit((FelderaBetweenOperation) expr); + } else if (expr instanceof FelderaFunctionCall) { + visit((FelderaFunctionCall) expr); + } else if (expr instanceof FelderaAggregate) { + visit((FelderaAggregate) expr); + } else { + throw new AssertionError(expr.toString()); + } + if (expr.isBlackbox()) { + sb.append(")"); + } + } + + private void visit(FelderaAggregate aggr) { + sb.append(aggr.getFunc().name()); + sb.append("("); + visit(aggr.getExpr()); + sb.append(")"); + } + + private void visit(FelderaCast cast) { + sb.append("CAST("); + visit(cast.getExpression()); + sb.append(cast.getStringRepresentation()); + sb.append(")"); + } + + private void visit(FelderaBetweenOperation op) { + sb.append("("); + visit(op.getExpr()); + sb.append(")"); + sb.append(" "); + sb.append(op.getType().getStringRepresentation()); + sb.append(" ("); + visit(op.getLeft()); + sb.append(") AND ("); + visit(op.getRight()); + sb.append(")"); + } + + private void visit(FelderaFunctionCall call) { + sb.append(call.getName()); + sb.append("("); + visit(call.getArguments()); + sb.append(")"); + } + + public void visit(FelderaJoin join) { + visit((TableReferenceNode) join.getLeftTable()); + sb.append(" "); + sb.append(join.getJoinType()); + sb.append(" JOIN "); + visit((TableReferenceNode) join.getRightTable()); + if (join.getOnCondition() != null) { + sb.append(" ON "); + visit(join.getOnCondition()); + } + } + + private void visit(FelderaConstant constant) { + sb.append(constant.toString()); + } + + private void visit(FelderaSelect select) { + sb.append("SELECT "); + if (select.isDistinct()) { + sb.append("DISTINCT "); + } + visit(select.getFetchColumns()); + sb.append(" FROM "); + visit(select.getFromList()); + if (!select.getFromList().isEmpty() && !select.getJoinList().isEmpty()) { + sb.append(", "); + } + if (!select.getJoinList().isEmpty()) { + visit(select.getJoinList()); + } + if (select.getWhereClause() != null) { + sb.append(" WHERE "); + visit(select.getWhereClause()); + } + if (!select.getGroupByExpressions().isEmpty()) { + sb.append(" GROUP BY "); + visit(select.getGroupByExpressions()); + } + if (select.getHavingClause() != null) { + sb.append(" HAVING "); + visit(select.getHavingClause()); + } + if (!select.getOrderByClauses().isEmpty()) { + sb.append(" ORDER BY "); + visit(select.getOrderByClauses()); + } + if (!select.getOrderByClauses().isEmpty()) { + sb.append(" LIMIT "); + visit(select.getOrderByClauses()); + } + if (select.getOffsetClause() != null) { + sb.append(" OFFSET "); + visit(select.getOffsetClause()); + } + } + + public static String asString(FelderaExpression expr) { + FelderaToStringVisitor visitor = new FelderaToStringVisitor(); + visitor.visit(expr); + return visitor.get(); + } +} diff --git a/src/sqlancer/feldera/ast/FelderaAggregate.java b/src/sqlancer/feldera/ast/FelderaAggregate.java new file mode 100644 index 000000000..410fc97d5 --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaAggregate.java @@ -0,0 +1,68 @@ +package sqlancer.feldera.ast; + +import sqlancer.feldera.FelderaSchema; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class FelderaAggregate implements FelderaExpression { + private final FelderaAggregateFunction func; + private final List expr; + private boolean blackbox; + + public enum FelderaAggregateFunction { + AVG(FelderaSchema.FelderaDataType.INT, FelderaSchema.FelderaDataType.DOUBLE), + COUNT(FelderaSchema.FelderaDataType.values()), EVERY(FelderaSchema.FelderaDataType.BOOLEAN), + MAX(FelderaSchema.FelderaDataType.values()), MIN(FelderaSchema.FelderaDataType.values()), + SOME(FelderaSchema.FelderaDataType.BOOLEAN), + SUM(FelderaSchema.FelderaDataType.INT, FelderaSchema.FelderaDataType.DOUBLE), + STDDEV(FelderaSchema.FelderaDataType.INT, FelderaSchema.FelderaDataType.DOUBLE), + STDDEV_POP(FelderaSchema.FelderaDataType.INT, FelderaSchema.FelderaDataType.DOUBLE),; + + private final FelderaSchema.FelderaDataType[] supportedReturnTypes; + + FelderaAggregateFunction(FelderaSchema.FelderaDataType... supportedReturnTypes) { + this.supportedReturnTypes = supportedReturnTypes.clone(); + } + + public List getTypes(FelderaSchema.FelderaDataType returnType) { + return Collections.singletonList(returnType); + } + + public boolean supportsReturnType(FelderaSchema.FelderaDataType returnType) { + return Arrays.stream(supportedReturnTypes).anyMatch(t -> t == returnType) + || supportedReturnTypes.length == 0; + } + + public static List getAggregates(FelderaSchema.FelderaDataType type) { + return Arrays.stream(values()).filter(p -> p.supportsReturnType(type)).collect(Collectors.toList()); + } + } + + public FelderaAggregate(FelderaAggregateFunction func, List expr) { + this.func = func; + this.expr = expr; + this.blackbox = false; + } + + @Override + public boolean isBlackbox() { + return blackbox; + } + + @Override + public void setBlackbox(boolean blackbox) { + this.blackbox = blackbox; + this.expr.forEach(f -> f.setBlackbox(blackbox)); + } + + public FelderaAggregateFunction getFunc() { + return func; + } + + public List getExpr() { + return expr; + } +} diff --git a/src/sqlancer/feldera/ast/FelderaBetweenOperation.java b/src/sqlancer/feldera/ast/FelderaBetweenOperation.java new file mode 100644 index 000000000..504144577 --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaBetweenOperation.java @@ -0,0 +1,52 @@ +package sqlancer.feldera.ast; + +import sqlancer.Randomly; + +public class FelderaBetweenOperation implements FelderaExpression { + private final FelderaExpression expr; + private final FelderaExpression left; + private final FelderaExpression right; + private FelderaBetweenOperatorType type; + + public enum FelderaBetweenOperatorType { + BETWEEN("BETWEEN"), NOT_BETWEEN("NOT BETWEEN"); + + private String s; + + FelderaBetweenOperatorType(String s) { + this.s = s; + } + + public String getStringRepresentation() { + return s; + } + + public static FelderaBetweenOperatorType getRandom() { + return Randomly.fromOptions(values()); + } + } + + public FelderaBetweenOperation(FelderaExpression expr, FelderaExpression left, FelderaExpression right, + FelderaBetweenOperatorType type) { + this.expr = expr; + this.left = left; + this.right = right; + this.type = type; + } + + public FelderaExpression getLeft() { + return left; + } + + public FelderaExpression getRight() { + return right; + } + + public FelderaExpression getExpr() { + return expr; + } + + public FelderaBetweenOperatorType getType() { + return type; + } +} diff --git a/src/sqlancer/feldera/ast/FelderaBinaryArithmeticOperation.java b/src/sqlancer/feldera/ast/FelderaBinaryArithmeticOperation.java new file mode 100644 index 000000000..b8db2da3f --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaBinaryArithmeticOperation.java @@ -0,0 +1,34 @@ +package sqlancer.feldera.ast; + +import sqlancer.Randomly; +import sqlancer.common.ast.BinaryOperatorNode; +import sqlancer.common.ast.newast.NewBinaryOperatorNode; + +public class FelderaBinaryArithmeticOperation extends NewBinaryOperatorNode + implements FelderaExpression { + + public enum FelderaBinaryArithmeticOperator implements BinaryOperatorNode.Operator { + ADD("+"), MULT("*"), MINUS("-"), DIV("/"), MOD("%"); + + String textRepresentation; + + FelderaBinaryArithmeticOperator(String textRepresentation) { + this.textRepresentation = textRepresentation; + } + + public static FelderaBinaryArithmeticOperator getRandom() { + return Randomly.fromOptions(values()); + } + + @Override + public String getTextRepresentation() { + return textRepresentation; + } + } + + public FelderaBinaryArithmeticOperation(FelderaExpression left, FelderaExpression right, + FelderaBinaryArithmeticOperator op) { + super(left, right, op); + } + +} diff --git a/src/sqlancer/feldera/ast/FelderaBinaryComparisonOperation.java b/src/sqlancer/feldera/ast/FelderaBinaryComparisonOperation.java new file mode 100644 index 000000000..e6ed87d0d --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaBinaryComparisonOperation.java @@ -0,0 +1,34 @@ +package sqlancer.feldera.ast; + +import sqlancer.Randomly; +import sqlancer.common.ast.BinaryOperatorNode; +import sqlancer.common.ast.newast.NewBinaryOperatorNode; + +public class FelderaBinaryComparisonOperation extends NewBinaryOperatorNode + implements FelderaExpression { + + public enum FelderaComparisonOperator implements BinaryOperatorNode.Operator { + EQUALS("="), GREATER(">"), GREATER_EQUALS(">="), SMALLER("<"), SMALLER_EQUALS("<="), NOT_EQUALS("!="), + IS_DISTINCT_FROM("IS DISTINCT FROM"), IS_NOT_DISTINCT_FROM("IS NOT DISTINCT FROM"); + + private String textRepr; + + FelderaComparisonOperator(String textRepr) { + this.textRepr = textRepr; + } + + public static FelderaComparisonOperator getRandom() { + return Randomly.fromOptions(values()); + } + + @Override + public String getTextRepresentation() { + return textRepr; + } + } + + public FelderaBinaryComparisonOperation(FelderaExpression left, FelderaExpression right, + FelderaComparisonOperator op) { + super(left, right, op); + } +} \ No newline at end of file diff --git a/src/sqlancer/feldera/ast/FelderaCast.java b/src/sqlancer/feldera/ast/FelderaCast.java new file mode 100644 index 000000000..e66f40920 --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaCast.java @@ -0,0 +1,22 @@ +package sqlancer.feldera.ast; + +import sqlancer.feldera.FelderaSchema; + +public class FelderaCast implements FelderaExpression { + private final FelderaExpression expr; + private final FelderaSchema.FelderaDataType type; + + public FelderaCast(FelderaExpression expr, FelderaSchema.FelderaDataType type) { + this.expr = expr; + this.type = type; + } + + public FelderaExpression getExpression() { + return expr; + } + + public String getStringRepresentation() { + return " AS " + type.toString(); + } + +} \ No newline at end of file diff --git a/src/sqlancer/feldera/ast/FelderaColumnReference.java b/src/sqlancer/feldera/ast/FelderaColumnReference.java new file mode 100644 index 000000000..ed1ab8d4e --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaColumnReference.java @@ -0,0 +1,12 @@ +package sqlancer.feldera.ast; + +import sqlancer.common.ast.newast.ColumnReferenceNode; +import sqlancer.feldera.FelderaSchema; + +public class FelderaColumnReference extends ColumnReferenceNode + implements FelderaExpression { + + public FelderaColumnReference(FelderaSchema.FelderaColumn felderaColumn) { + super(felderaColumn); + } +} diff --git a/src/sqlancer/feldera/ast/FelderaConstant.java b/src/sqlancer/feldera/ast/FelderaConstant.java new file mode 100644 index 000000000..f7050ac8e --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaConstant.java @@ -0,0 +1,315 @@ +package sqlancer.feldera.ast; + +import sqlancer.Randomly; +import sqlancer.feldera.FelderaGlobalState; +import sqlancer.feldera.FelderaSchema; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; + +public abstract class FelderaConstant implements FelderaExpression { + private FelderaConstant() { + } + + private static double round(double number, int places) { + BigDecimal decimal = new BigDecimal(number); + decimal = decimal.setScale(places, RoundingMode.HALF_UP); + return decimal.doubleValue(); + } + + public static FelderaConstant getRandomConstant(FelderaGlobalState globalState, + FelderaSchema.FelderaDataType type) { + switch (type) { + case BOOLEAN: + return new FelderaBooleanConstant(Randomly.getBoolean()); + case TINYINT: + return FelderaIntConstant.getRandom(globalState, 8); + case SMALLINT: + return FelderaIntConstant.getRandom(globalState, 16); + case INT: + return FelderaIntConstant.getRandom(globalState, 32); + case BIGINT: + return FelderaIntConstant.getRandom(globalState, 64); + case VARCHAR: + return FelderaVarcharConstant.getRandom(globalState); + case CHAR: + return FelderaCharConstant.getRandom(globalState); + case NULL: + return new FelderaNullConstant(); + case TIME: + return FelderaTimeConstant.getRandom(globalState); + case DATE: + return FelderaDateConstant.getRandom(globalState); + case TIMESTAMP: + return FelderaTimestampConstant.getRandom(globalState); + case REAL: + return FelderaRealConstant.getRandom(globalState); + case DOUBLE: + return FelderaDoubleConstant.getRandom(globalState); + default: + throw new AssertionError(type); + } + } + + public static class FelderaTimeConstant extends FelderaConstant { + private final String value; + + FelderaTimeConstant(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return "'" + value + "'"; + } + + public static FelderaTimeConstant getRandom(FelderaGlobalState globalState) { + Randomly r = globalState.getRandomly(); + int h = r.getInteger(0, 23); + int m = r.getInteger(0, 59); + int s = r.getInteger(0, 59); + + return new FelderaTimeConstant(h + ":" + m + ":" + s); + } + } + + public static class FelderaDateConstant extends FelderaConstant { + private final String value; + + FelderaDateConstant(String value) { + this.value = value; + } + + @Override + public String toString() { + return "'" + value + "'"; + } + + public static FelderaDateConstant getRandom(FelderaGlobalState globalState) { + Randomly r = globalState.getRandomly(); + + int year = r.getInteger(0, 9999); + int month = r.getInteger(1, 12); + int day = r.getInteger(1, 31); + + return new FelderaDateConstant(year + "-" + month + "-" + day); + } + } + + public static class FelderaTimestampConstant extends FelderaConstant { + private final String value; + + FelderaTimestampConstant(String value) { + this.value = value; + } + + @Override + public String toString() { + return "'" + value + "'"; + } + + public static FelderaTimestampConstant getRandom(FelderaGlobalState globalState) { + String date = FelderaDateConstant.getRandom(globalState).value; + String time = FelderaTimeConstant.getRandom(globalState).value; + + return new FelderaTimestampConstant(date + " " + time); + } + } + + private static class FelderaNullConstant extends FelderaConstant { + @Override + public String toString() { + return "NULL"; + } + } + + public static class FelderaIntConstant extends FelderaConstant { + private final long value; + + public FelderaIntConstant(long value) { + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public long getValue() { + return value; + } + + public static FelderaIntConstant getRandom(FelderaGlobalState globalState) { + return new FelderaIntConstant(globalState.getRandomly().getInteger()); + } + + public static FelderaIntConstant getRandom(FelderaGlobalState globalState, int bitLength) { + // int left = -(1 << (bitLength - 1)); + // int right = (1 << (bitLength - 1)) - 1; + // return new FelderaIntConstant(globalState.getRandomly().getLong(left, right)); + // HACK: for now, generate just a small random number that isn't 0 + return new FelderaIntConstant(Randomly.smallNumber() + 1); + } + } + + public static class FelderaDoubleConstant extends FelderaConstant { + private final double value; + + public FelderaDoubleConstant(double value) { + this.value = value; + } + + public double getValue() { + return value; + } + + @Override + public String toString() { + if (value == Double.POSITIVE_INFINITY) { + return "'+Inf'"; + } else if (value == Double.NEGATIVE_INFINITY) { + return "'-Inf'"; + } + return String.valueOf(value); + } + + public static FelderaDoubleConstant getRandom(FelderaGlobalState globalState) { + return new FelderaDoubleConstant( + FelderaConstant.round(globalState.getRandomly().getFiniteDouble() + 1.0, 10)); + } + } + + public static class FelderaRealConstant extends FelderaConstant { + private final float value; + + public FelderaRealConstant(float value) { + this.value = value; + } + + public float getValue() { + return value; + } + + @Override + public String toString() { + if (value == Float.POSITIVE_INFINITY) { + return "'+Inf'"; + } else if (value == Float.NEGATIVE_INFINITY) { + return "'-Inf'"; + } + return String.valueOf(value); + } + + public static FelderaRealConstant getRandom(FelderaGlobalState globalState) { + return new FelderaRealConstant( + ((float) FelderaConstant.round(globalState.getRandomly().getFiniteDouble() + 1.0, 5))); + } + } + + public static class FelderaVarcharConstant extends FelderaConstant { + private final String value; + + public FelderaVarcharConstant(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return "'" + value.replace("'", "''") + "'"; + } + + private static String getRandomString(FelderaGlobalState globalState) { + return globalState.getRandomly().getString().replaceAll("[^a-zA-Z0-9]", ""); + } + + public static FelderaVarcharConstant getRandom(FelderaGlobalState globalState) { + String randomString = getRandomString(globalState); + + // retry for 10 times, but if it's still empty, just use a default string + for (int i = 0; i < 10; i++) { + if (!randomString.isBlank()) { + break; + } + randomString = getRandomString(globalState); + } + + if (randomString.isBlank()) { + randomString = "DEFAULT STRING"; + } + + return new FelderaVarcharConstant(randomString); + } + } + + public static class FelderaCharConstant extends FelderaConstant { + private final char value; + + public FelderaCharConstant(char value) { + this.value = value; + } + + public char getValue() { + return value; + } + + @Override + public String toString() { + return "'" + this.value + "'"; + } + + public static FelderaCharConstant getRandom(FelderaGlobalState globalState) { + char ch = globalState.getRandomly().getAlphabeticChar().charAt(0); + while (true) { + if (StandardCharsets.ISO_8859_1.newEncoder().canEncode(ch)) { + return new FelderaCharConstant(ch); + } + } + } + } + + public static class FelderaBooleanConstant extends FelderaConstant { + private final boolean value; + + public FelderaBooleanConstant(boolean value) { + this.value = value; + } + + public boolean getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + + public static FelderaExpression createNullConstant() { + return new FelderaNullConstant(); + } + + public static FelderaExpression createVarcharConstant(String text) { + return new FelderaVarcharConstant(text); + } + + public static FelderaExpression createDoubleConstant(double val) { + return new FelderaDoubleConstant(val); + } + + public static FelderaExpression createIntConstant(long val) { + return new FelderaIntConstant(val); + } + + public static FelderaExpression createBooleanConstant(boolean val) { + return new FelderaBooleanConstant(val); + } +} diff --git a/src/sqlancer/feldera/ast/FelderaExpression.java b/src/sqlancer/feldera/ast/FelderaExpression.java new file mode 100644 index 000000000..508562127 --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaExpression.java @@ -0,0 +1,13 @@ +package sqlancer.feldera.ast; + +import sqlancer.common.ast.newast.Expression; +import sqlancer.feldera.FelderaSchema; + +public interface FelderaExpression extends Expression { + default public boolean isBlackbox() { + return false; + } + + default public void setBlackbox(boolean blackbox) { + } +} diff --git a/src/sqlancer/feldera/ast/FelderaFunction.java b/src/sqlancer/feldera/ast/FelderaFunction.java new file mode 100644 index 000000000..204d8712e --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaFunction.java @@ -0,0 +1,135 @@ +package sqlancer.feldera.ast; + +import sqlancer.feldera.FelderaSchema; +import sqlancer.feldera.gen.FelderaExpressionGenerator; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public enum FelderaFunction { + // String + ASCII(FelderaSchema.FelderaDataType.INT, FelderaSchema.FelderaDataType.VARCHAR), + CHR(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.INT), + CONCAT(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR, + FelderaSchema.FelderaDataType.VARCHAR), + INITCAP(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR), + LEFT(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR, + FelderaSchema.FelderaDataType.INT), + LOWER(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR), + REPEAT(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR, + FelderaSchema.FelderaDataType.INT), + RLIKE(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR, + FelderaSchema.FelderaDataType.VARCHAR), + SUBSTRING(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR, + FelderaSchema.FelderaDataType.INT), + TRIM(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR), + UPPER(FelderaSchema.FelderaDataType.VARCHAR, FelderaSchema.FelderaDataType.VARCHAR), + + // Double + ABS(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + CEIL(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + FLOOR(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + TRUNCATE1("TRUNCATE", FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + // TRUNCATE2("TRUNCATE", FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE, + // FelderaSchema.FelderaDataType.INT), + ROUND1("ROUND", FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + // ROUND2("ROUND", FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE, + // FelderaSchema.FelderaDataType.INT), + POWER(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE, + FelderaSchema.FelderaDataType.DOUBLE), + POWER2("POWER", FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.INT, + FelderaSchema.FelderaDataType.INT), + SQRT(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + EXP(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + LN(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + LOG(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + LOG10(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + IS_INF(FelderaSchema.FelderaDataType.BOOLEAN, FelderaSchema.FelderaDataType.DOUBLE), + IS_NAN(FelderaSchema.FelderaDataType.BOOLEAN, FelderaSchema.FelderaDataType.DOUBLE), + SIN(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + COS(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + TAN(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + COT(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + SEC(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + CSC(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + ASIN(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + ACOS(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + ATAN(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + ATAN2(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE, + FelderaSchema.FelderaDataType.DOUBLE), + DEGREES(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + RADIANS(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + CBRT(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + SINH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + COSH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + TANH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + COTH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + SECH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + CSCH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + ASINH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + ACOSH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + ATANH(FelderaSchema.FelderaDataType.DOUBLE, FelderaSchema.FelderaDataType.DOUBLE), + + // Int + ABS_INT("abs", FelderaSchema.FelderaDataType.INT, FelderaSchema.FelderaDataType.INT), + MOD(FelderaSchema.FelderaDataType.INT, FelderaSchema.FelderaDataType.INT, FelderaSchema.FelderaDataType.INT),; + + private FelderaSchema.FelderaDataType returnType; + private FelderaSchema.FelderaDataType[] argumentTypes; + private String functionName; + + FelderaFunction(FelderaSchema.FelderaDataType returnType, FelderaSchema.FelderaDataType... argumentTypes) { + this.returnType = returnType; + this.argumentTypes = argumentTypes.clone(); + this.functionName = toString(); + } + + FelderaFunction(FelderaSchema.FelderaDataType returnType) { + this.returnType = returnType; + this.argumentTypes = new FelderaSchema.FelderaDataType[0]; + this.functionName = toString(); + } + + FelderaFunction(String functionName, FelderaSchema.FelderaDataType returnType, + FelderaSchema.FelderaDataType... argumentTypes) { + this.functionName = functionName; + this.returnType = returnType; + this.argumentTypes = argumentTypes.clone(); + } + + public boolean isCompatibleWithReturnType(FelderaSchema.FelderaDataType dataType) { + return this.returnType == dataType; + } + + public String getFunctionName() { + return functionName; + } + + public FelderaSchema.FelderaDataType[] getArgumentTypes() { + return argumentTypes; + } + + public FelderaFunctionCall getCall(FelderaSchema.FelderaDataType returnType, FelderaExpressionGenerator gen, + int depth) { + FelderaSchema.FelderaDataType[] argumentTypes = getArgumentTypes(); + List arguments = getArgumentsForReturnType(gen, depth, argumentTypes, returnType); + return new FelderaFunctionCall(this, arguments); + } + + List getArgumentsForReturnType(FelderaExpressionGenerator gen, int depth, + FelderaSchema.FelderaDataType[] argumentTypes, FelderaSchema.FelderaDataType returnType) { + List arguments = new ArrayList<>(); + + for (FelderaSchema.FelderaDataType arg : argumentTypes) { + arguments.add(gen.generateExpression(arg, depth + 1)); + } + + return arguments; + } + + public static List getFunctionCompatibleWith(FelderaSchema.FelderaDataType returnType) { + return Stream.of(values()).filter(f -> f.isCompatibleWithReturnType(returnType)).collect(Collectors.toList()); + } +} diff --git a/src/sqlancer/feldera/ast/FelderaFunctionCall.java b/src/sqlancer/feldera/ast/FelderaFunctionCall.java new file mode 100644 index 000000000..0adf6f6cc --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaFunctionCall.java @@ -0,0 +1,38 @@ +package sqlancer.feldera.ast; + +import java.util.List; + +public class FelderaFunctionCall implements FelderaExpression { + private final FelderaFunction function; + private final List arguments; + private boolean blackbox; + + public FelderaFunctionCall(FelderaFunction function, List arguments) { + this.function = function; + this.arguments = arguments; + this.blackbox = false; + } + + @Override + public boolean isBlackbox() { + return blackbox; + } + + @Override + public void setBlackbox(boolean blackbox) { + this.blackbox = blackbox; + this.arguments.forEach(arg -> arg.setBlackbox(blackbox)); + } + + public List getArguments() { + return arguments; + } + + public FelderaFunction getFunction() { + return function; + } + + public String getName() { + return function.getFunctionName(); + } +} diff --git a/src/sqlancer/feldera/ast/FelderaJoin.java b/src/sqlancer/feldera/ast/FelderaJoin.java new file mode 100644 index 000000000..f56af719c --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaJoin.java @@ -0,0 +1,51 @@ +package sqlancer.feldera.ast; + +import sqlancer.Randomly; +import sqlancer.common.ast.newast.Join; +import sqlancer.feldera.FelderaSchema; + +public class FelderaJoin + implements FelderaExpression, Join { + + private final FelderaTableReference leftTable; + private final FelderaTableReference rightTable; + private final FelderaJoinType joinType; + private FelderaExpression onCondition; + + public enum FelderaJoinType { + INNER, NATURAL, LEFT, RIGHT; + + public static FelderaJoinType getRandom() { + return Randomly.fromOptions(values()); + } + } + + public FelderaJoin(FelderaTableReference leftTable, FelderaTableReference rightTable, FelderaJoinType joinType, + FelderaExpression whereCondition) { + this.leftTable = leftTable; + this.rightTable = rightTable; + this.joinType = joinType; + this.onCondition = whereCondition; + } + + public FelderaTableReference getLeftTable() { + return leftTable; + } + + public FelderaTableReference getRightTable() { + return rightTable; + } + + public FelderaJoinType getJoinType() { + return joinType; + } + + public FelderaExpression getOnCondition() { + return onCondition; + } + + @Override + public void setOnClause(FelderaExpression onClause) { + this.onCondition = onClause; + } +} diff --git a/src/sqlancer/feldera/ast/FelderaSelect.java b/src/sqlancer/feldera/ast/FelderaSelect.java new file mode 100644 index 000000000..8c83ab626 --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaSelect.java @@ -0,0 +1,40 @@ +package sqlancer.feldera.ast; + +import sqlancer.common.ast.SelectBase; +import sqlancer.common.ast.newast.Select; +import sqlancer.feldera.FelderaSchema; +import sqlancer.feldera.FelderaToStringVisitor; + +import java.util.List; +import java.util.stream.Collectors; + +public class FelderaSelect extends SelectBase + implements Select, + FelderaExpression { + private boolean isDistinct; + + public void setDistinct(boolean isDistinct) { + this.isDistinct = isDistinct; + } + + public boolean isDistinct() { + return isDistinct; + } + + @Override + public void setJoinClauses(List joinStatements) { + List expressions = joinStatements.stream().map(e -> (FelderaExpression) e) + .collect(Collectors.toList()); + setJoinList(expressions); + } + + @Override + public List getJoinClauses() { + return getJoinList().stream().map(e -> (FelderaJoin) e).collect(Collectors.toList()); + } + + @Override + public String asString() { + return FelderaToStringVisitor.asString(this); + } +} \ No newline at end of file diff --git a/src/sqlancer/feldera/ast/FelderaTableReference.java b/src/sqlancer/feldera/ast/FelderaTableReference.java new file mode 100644 index 000000000..8b343b5bb --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaTableReference.java @@ -0,0 +1,21 @@ +package sqlancer.feldera.ast; + +import sqlancer.common.ast.newast.TableReferenceNode; +import sqlancer.feldera.FelderaSchema; + +public class FelderaTableReference extends TableReferenceNode + implements FelderaExpression { + public FelderaTableReference(FelderaSchema.FelderaTable table) { + super(table); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FelderaTableReference)) { + return false; + } + + FelderaTableReference other = (FelderaTableReference) obj; + return this.getTable().equals(other.getTable()); + } +} diff --git a/src/sqlancer/feldera/ast/FelderaUnaryPostfixOperation.java b/src/sqlancer/feldera/ast/FelderaUnaryPostfixOperation.java new file mode 100644 index 000000000..074e2f831 --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaUnaryPostfixOperation.java @@ -0,0 +1,28 @@ +package sqlancer.feldera.ast; + +import sqlancer.common.ast.BinaryOperatorNode; +import sqlancer.common.ast.newast.NewUnaryPostfixOperatorNode; + +public class FelderaUnaryPostfixOperation extends NewUnaryPostfixOperatorNode + implements FelderaExpression { + + public enum FelderaUnaryPostfixOperator implements BinaryOperatorNode.Operator { + IS_NULL("IS NULL"), IS_NOT_NULL("IS NOT NULL"), IS_FALSE("IS FALSE"), IS_NOT_TRUE("IS NOT TRUE"), + IS_NOT_FALSE("IS NOT FALSE"); + + private String s; + + FelderaUnaryPostfixOperator(String s) { + this.s = s; + } + + @Override + public String getTextRepresentation() { + return s; + } + } + + public FelderaUnaryPostfixOperation(FelderaExpression expr, FelderaUnaryPostfixOperator op) { + super(expr, op); + } +} diff --git a/src/sqlancer/feldera/ast/FelderaUnaryPrefixOperation.java b/src/sqlancer/feldera/ast/FelderaUnaryPrefixOperation.java new file mode 100644 index 000000000..062914fec --- /dev/null +++ b/src/sqlancer/feldera/ast/FelderaUnaryPrefixOperation.java @@ -0,0 +1,27 @@ +package sqlancer.feldera.ast; + +import sqlancer.common.ast.BinaryOperatorNode; +import sqlancer.common.ast.newast.NewUnaryPrefixOperatorNode; + +public class FelderaUnaryPrefixOperation extends NewUnaryPrefixOperatorNode + implements FelderaExpression { + + public FelderaUnaryPrefixOperation(FelderaExpression expr, BinaryOperatorNode.Operator op) { + super(expr, op); + } + + public enum FelderaUnaryPrefixOperator implements BinaryOperatorNode.Operator { + NOT("NOT"), UNARY_PLUS("+"), UNARY_MINUS("-"); + + private String textRepresentation; + + FelderaUnaryPrefixOperator(String textRepresentation) { + this.textRepresentation = textRepresentation; + } + + @Override + public String getTextRepresentation() { + return this.textRepresentation; + } + } +} diff --git a/src/sqlancer/feldera/client/FelderaClient.java b/src/sqlancer/feldera/client/FelderaClient.java new file mode 100644 index 000000000..83efdffd4 --- /dev/null +++ b/src/sqlancer/feldera/client/FelderaClient.java @@ -0,0 +1,87 @@ +package sqlancer.feldera.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.MapType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class FelderaClient { + private final HttpRequests httpRequests; + + public FelderaClient(String url) { + this.httpRequests = new HttpRequests(url + "/v0"); + } + + public FelderaPipeline getPipeline(String name) throws Exception { + String resp = this.httpRequests.get(String.format("/pipelines/%s", name)); + return FelderaPipeline.fromJson(resp); + } + + private void waitForCompilation(String name) throws Exception { + List wait = Arrays.asList("Pending", "CompilingSql", "SqlCompiled", "CompilingRust"); + + while (true) { + FelderaPipeline resp = this.getPipeline(name); + String status = resp.getProgramStatus(); + + if (Objects.equals(status, "Success")) { + return; + } else if (!wait.contains(status)) { + throw new AssertionError(String.format("err: pipeline: %s failed to compile: %s", name, status)); + } + + Thread.sleep(500); + } + } + + public Map exec(String pipelineName, String sql) throws Exception { + Map options = new HashMap<>(); + options.put("sql", URLEncoder.encode(sql, StandardCharsets.UTF_8)); + options.put("format", "json"); + + String resp = this.httpRequests.get(String.format("/pipelines/%s/query", pipelineName), options); + if (resp.isBlank()) { + return Collections.emptyMap(); + } + + ObjectMapper mapper = new ObjectMapper(); + TypeFactory typeFactory = mapper.getTypeFactory(); + MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Object.class); + + return mapper.readValue(resp, mapType); + } + + private void blockTillDesiredState(String pipelineName, String desired) throws Exception { + while (true) { + String deploymentStatus = this.getPipeline(pipelineName).getDeploymentStatus(); + if (deploymentStatus.equalsIgnoreCase(desired)) { + break; + } + + Thread.sleep(500); + } + } + + public void createPipeline(String pipelineName, String body) throws Exception { + this.httpRequests.put(String.format("/pipelines/%s", pipelineName), body); + waitForCompilation(pipelineName); + } + + public void start(String pipelineName) throws Exception { + this.httpRequests.post(String.format("/pipelines/%s/start", pipelineName), Collections.emptyMap()); + blockTillDesiredState(pipelineName, "running"); + } + + public void pause(String pipelineName) throws Exception { + this.httpRequests.post(String.format("/pipelines/%s/pause", pipelineName), Collections.emptyMap()); + blockTillDesiredState(pipelineName, "paused"); + } + + public void shutdown(String pipelineName) throws Exception { + this.httpRequests.post(String.format("/pipelines/%s/shutdown", pipelineName), Collections.emptyMap()); + blockTillDesiredState(pipelineName, "shutdown"); + } +} \ No newline at end of file diff --git a/src/sqlancer/feldera/client/FelderaException.java b/src/sqlancer/feldera/client/FelderaException.java new file mode 100644 index 000000000..876a8bcf6 --- /dev/null +++ b/src/sqlancer/feldera/client/FelderaException.java @@ -0,0 +1,19 @@ +package sqlancer.feldera.client; + +public class FelderaException extends Exception { + private static final long serialVersionUID = 1L; + + public FelderaException(int statusCode) { + super("got status code: " + statusCode); + } + + @SuppressWarnings("unused") + public FelderaException(String message) { + super(message); + } + + @SuppressWarnings("unused") + public FelderaException(String message, int statusCode) { + super("got status code: " + statusCode + "; error message: " + message); + } +} diff --git a/src/sqlancer/feldera/client/FelderaPipeline.java b/src/sqlancer/feldera/client/FelderaPipeline.java new file mode 100644 index 000000000..06e627254 --- /dev/null +++ b/src/sqlancer/feldera/client/FelderaPipeline.java @@ -0,0 +1,68 @@ +package sqlancer.feldera.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.MapType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Optional; + +public class FelderaPipeline { + private final String name; + private final String programCode; + private final String deploymentStatus; + private final String programStatus; + private final String deploymentError; + private final int version; + + public FelderaPipeline(HashMap resp) throws FelderaException { + this.name = resp.get("name").toString(); + this.programCode = resp.get("program_code").toString(); + this.deploymentStatus = resp.get("deployment_status").toString(); + this.deploymentError = Optional.ofNullable(resp.get("deployment_error")).map(Object::toString).orElse(""); + this.version = Integer.parseInt(resp.get("version").toString()); + + if (resp.get("program_status") instanceof String) { + this.programStatus = resp.get("program_status").toString(); + } else { + throw new FelderaException(String.format("err: pipeline %s failed to compile:\n%s", name, + resp.get("program_status").toString())); + } + } + + public static FelderaPipeline fromJson(String json) throws JsonProcessingException, FelderaException { + ObjectMapper objectMapper = new ObjectMapper(); + TypeFactory typeFactory = objectMapper.getTypeFactory(); + MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Object.class); + + return new FelderaPipeline(objectMapper.readValue(json, mapType)); + } + + public String getName() { + return name; + } + + public String getProgramCode() { + return programCode; + } + + public String getDeploymentStatus() { + return deploymentStatus; + } + + public String getProgramStatus() { + return programStatus; + } + + @Nullable + public String getDeploymentError() { + return deploymentError; + } + + public int getVersion() { + return this.version; + } + +} diff --git a/src/sqlancer/feldera/client/HttpRequests.java b/src/sqlancer/feldera/client/HttpRequests.java new file mode 100644 index 000000000..cbd9aa07e --- /dev/null +++ b/src/sqlancer/feldera/client/HttpRequests.java @@ -0,0 +1,105 @@ +package sqlancer.feldera.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpTimeoutException; +import java.time.Duration; +import java.util.Map; + +public class HttpRequests { + private final String baseUrl; + private final ObjectMapper objectMapper; + private final HttpClient httpClient; + + public HttpRequests(String baseUrl) { + this.baseUrl = baseUrl; + this.objectMapper = new ObjectMapper(); + this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(); + } + + private HttpRequest.Builder createRequestBuilder(String path) { + return HttpRequest.newBuilder().timeout(Duration.ofSeconds(2)).uri(URI.create(baseUrl + path)) + .header("Content-Type", "application/json").header("User-Agent", "feldera-java-client/v1"); + } + + private HttpRequest.BodyPublisher serializeBody(T body) throws JsonProcessingException { + if (body == null) { + return HttpRequest.BodyPublishers.noBody(); + } else if (body instanceof String) { + return HttpRequest.BodyPublishers.ofString((String) body); + } else { + return HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)); + } + } + + private void validateResponse(HttpResponse response) throws Exception { + int statusCode = response.statusCode(); + if (statusCode >= 200 && statusCode < 300) { + return; // Successful response + } + + String contentType = response.headers().firstValue("content-type").orElse(""); + String errorMessage; + + if (contentType.equals("application/json")) { + errorMessage = objectMapper.readTree(response.body()).toPrettyString(); + } else { + errorMessage = response.body(); + } + + throw new Exception("HTTP Error: " + statusCode + " " + errorMessage); + } + + private String sendRequest(HttpRequest request) throws Exception { + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + validateResponse(response); + return response.body(); + } catch (HttpTimeoutException e) { + throw new Exception("Request timed out: " + e.getMessage(), e); + } catch (Exception e) { + String path = request.uri().getPath(); + throw new Exception("Failed to send request: " + path + "\nErr: " + e.getMessage(), e); + } + } + + public String get(String path) throws Exception { + HttpRequest request = createRequestBuilder(path).GET().build(); + return sendRequest(request); + } + + public String get(String path, Map queryParams) throws Exception { + if (queryParams == null) { + return get(path); + } + + String q = "?" + queryParams.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()) + .reduce((a, b) -> a + "&" + b).orElse(""); + + HttpRequest request = createRequestBuilder(path + q).GET().build(); + return sendRequest(request); + } + + public String post(String path, T body) throws Exception { + HttpRequest request = createRequestBuilder(path).POST(serializeBody(body)).build(); + + return sendRequest(request); + } + + public String patch(String path, T body) throws Exception { + HttpRequest request = createRequestBuilder(path).method("PATCH", serializeBody(body)).build(); + + return sendRequest(request); + } + + public String put(String path, T body) throws Exception { + HttpRequest request = createRequestBuilder(path).PUT(serializeBody(body)).build(); + + return sendRequest(request); + } +} diff --git a/src/sqlancer/feldera/client/TestFelderaClient.java b/src/sqlancer/feldera/client/TestFelderaClient.java new file mode 100644 index 000000000..3121d5f81 --- /dev/null +++ b/src/sqlancer/feldera/client/TestFelderaClient.java @@ -0,0 +1,52 @@ +package sqlancer.feldera.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; + +import java.util.Map; + +public class TestFelderaClient { + private static final ObjectMapper mapper = new ObjectMapper(); + private static final FelderaClient client = new FelderaClient("http://localhost:8080"); + + @Test + public void testCreatePipeline() throws Exception { + String name = "testpipeline0"; + + ObjectNode node = mapper.createObjectNode(); + node.put("name", name); + node.put("description", "sqlancerTest"); + node.put("program_code", ""); + node.putObject("runtime_config"); + node.putObject("program_config"); + + client.createPipeline(name, node.toString()); + } + + @Test + public void testGetPipeline() throws Exception { + String name = "testpipeline0"; + FelderaPipeline p = client.getPipeline(name); + + assert p.getName().equals(name); + } + + @Test + public void testStateChanges() throws Exception { + String name = "testpipeline0"; + client.start(name); + client.pause(name); + client.shutdown(name); + } + + @Test + public void testSelect() throws Exception { + String name = "testpipeline0"; + client.start(name); + Map resp = client.exec(name, "select 1 as c"); + client.shutdown(name); + + assert (Integer) resp.get("c") == 1; + } +} diff --git a/src/sqlancer/feldera/client/TestFelderaPipeline.java b/src/sqlancer/feldera/client/TestFelderaPipeline.java new file mode 100644 index 000000000..e8239ee43 --- /dev/null +++ b/src/sqlancer/feldera/client/TestFelderaPipeline.java @@ -0,0 +1,69 @@ +package sqlancer.feldera.client; + +import org.junit.Test; + +import java.util.Objects; + +public class TestFelderaPipeline { + @Test + public void testPipelineFromJson() throws Exception { + String json = "{\n" + " \"id\": \"01939c8a-e0c4-7410-a3e3-e46b700540c8\",\n" + " \"name\": \"database0\",\n" + + " \"description\": \"sqlancer test\",\n" + " \"created_at\": \"2024-12-06T15:16:07.492687Z\",\n" + + " \"version\": 51,\n" + " \"platform_version\": \"0.31.1\",\n" + " \"runtime_config\": {\n" + + " \"workers\": 8,\n" + " \"storage\": false,\n" + " \"fault_tolerance\": null,\n" + + " \"cpu_profiler\": true,\n" + " \"tracing\": false,\n" + + " \"tracing_endpoint_jaeger\": \"127.0.0.1:6831\",\n" + " \"min_batch_size_records\": 0,\n" + + " \"max_buffering_delay_usecs\": 0,\n" + " \"resources\": {\n" + + " \"cpu_cores_min\": null,\n" + " \"cpu_cores_max\": null,\n" + + " \"memory_mb_min\": null,\n" + " \"memory_mb_max\": null,\n" + + " \"storage_mb_max\": null,\n" + " \"storage_class\": null\n" + " },\n" + + " \"min_storage_bytes\": null,\n" + " \"clock_resolution_usecs\": 100000\n" + " },\n" + + " \"program_code\": \"CREATE VIEW v AS SELECT 1;\",\n" + " \"udf_rust\": \"\",\n" + + " \"udf_toml\": \"\",\n" + " \"program_config\": {\n" + " \"profile\": null,\n" + + " \"cache\": true\n" + " },\n" + " \"program_version\": 51,\n" + + " \"program_status\": \"Success\",\n" + + " \"program_status_since\": \"2024-12-06T19:55:50.527299Z\",\n" + " \"program_info\": {\n" + + " \"schema\": {\n" + " \"inputs\": [],\n" + " \"outputs\": [\n" + " {\n" + + " \"name\": \"error_view\",\n" + " \"case_sensitive\": false,\n" + + " \"fields\": [\n" + " {\n" + " \"name\": \"table_or_view_name\",\n" + + " \"case_sensitive\": false,\n" + " \"columntype\": {\n" + + " \"type\": \"VARCHAR\",\n" + " \"nullable\": false,\n" + + " \"precision\": -1,\n" + " \"scale\": null,\n" + + " \"component\": null,\n" + " \"fields\": null,\n" + + " \"key\": null,\n" + " \"value\": null\n" + " }\n" + + " },\n" + " {\n" + " \"name\": \"message\",\n" + + " \"case_sensitive\": false,\n" + " \"columntype\": {\n" + + " \"type\": \"VARCHAR\",\n" + " \"nullable\": false,\n" + + " \"precision\": -1,\n" + " \"scale\": null,\n" + + " \"component\": null,\n" + " \"fields\": null,\n" + + " \"key\": null,\n" + " \"value\": null\n" + " }\n" + + " },\n" + " {\n" + " \"name\": \"metadata\",\n" + + " \"case_sensitive\": false,\n" + " \"columntype\": {\n" + + " \"type\": \"VARIANT\",\n" + " \"nullable\": false,\n" + + " \"precision\": null,\n" + " \"scale\": null,\n" + + " \"component\": null,\n" + " \"fields\": null,\n" + + " \"key\": null,\n" + " \"value\": null\n" + " }\n" + + " }\n" + " ],\n" + " \"materialized\": false,\n" + + " \"properties\": {}\n" + " },\n" + " {\n" + " \"name\": \"v\",\n" + + " \"case_sensitive\": false,\n" + " \"fields\": [\n" + " {\n" + + " \"name\": \"EXPR$0\",\n" + " \"case_sensitive\": false,\n" + + " \"columntype\": {\n" + " \"type\": \"INTEGER\",\n" + + " \"nullable\": false,\n" + " \"precision\": null,\n" + + " \"scale\": null,\n" + " \"component\": null,\n" + + " \"fields\": null,\n" + " \"key\": null,\n" + + " \"value\": null\n" + " }\n" + " }\n" + " ],\n" + + " \"materialized\": false,\n" + " \"properties\": {}\n" + " }\n" + + " ]\n" + " },\n" + " \"input_connectors\": {},\n" + " \"output_connectors\": {}\n" + + " },\n" + " \"deployment_status\": \"Shutdown\",\n" + + " \"deployment_status_since\": \"2024-12-06T19:55:26.508696Z\",\n" + + " \"deployment_desired_status\": \"Shutdown\",\n" + " \"deployment_error\": null,\n" + + " \"deployment_config\": null,\n" + " \"deployment_location\": null\n" + "}"; + + FelderaPipeline p = FelderaPipeline.fromJson(json); + assert Objects.equals(p.getName(), "database0"); + assert Objects.equals(p.getProgramCode(), "CREATE VIEW v AS SELECT 1;"); + assert Objects.equals(p.getDeploymentStatus(), "Shutdown"); + assert Objects.equals(p.getDeploymentError(), ""); + assert Objects.equals(p.getProgramStatus(), "Success"); + } +} diff --git a/src/sqlancer/feldera/gen/FelderaCommon.java b/src/sqlancer/feldera/gen/FelderaCommon.java new file mode 100644 index 000000000..af7f12061 --- /dev/null +++ b/src/sqlancer/feldera/gen/FelderaCommon.java @@ -0,0 +1,12 @@ +package sqlancer.feldera.gen; + +import sqlancer.feldera.FelderaSchema; + +public class FelderaCommon { + private FelderaCommon() { + } + + public static void appendDataType(FelderaSchema.FelderaDataType type, StringBuilder sb) throws AssertionError { + sb.append(type.toString()); + } +} diff --git a/src/sqlancer/feldera/gen/FelderaExpressionGenerator.java b/src/sqlancer/feldera/gen/FelderaExpressionGenerator.java new file mode 100644 index 000000000..83b978319 --- /dev/null +++ b/src/sqlancer/feldera/gen/FelderaExpressionGenerator.java @@ -0,0 +1,341 @@ +package sqlancer.feldera.gen; + +import sqlancer.Randomly; +import sqlancer.common.gen.NoRECGenerator; +import sqlancer.common.gen.TypedExpressionGenerator; +import sqlancer.common.schema.AbstractTables; +import sqlancer.feldera.FelderaGlobalState; +import sqlancer.feldera.FelderaSchema; +import sqlancer.feldera.ast.*; + +import java.util.*; +import java.util.stream.Collectors; + +public final class FelderaExpressionGenerator + extends TypedExpressionGenerator + implements + NoRECGenerator { + + private List tables; + private final FelderaGlobalState globalState; + private final int maxDepth; + + public FelderaExpressionGenerator(FelderaGlobalState globalState) { + this.globalState = globalState; + this.maxDepth = globalState.getOptions().getMaxExpressionDepth(); + } + + private enum BooleanExpression { + NOT, COMPARISON, IS_NULL, BETWEEN; + } + + private FelderaExpression getBinaryComparison(int depth) { + FelderaSchema.FelderaDataType type = getRandomType(); + FelderaExpression left = generateExpression(type, depth + 1); + FelderaExpression right = generateExpression(type, depth + 1); + return new FelderaBinaryComparisonOperation(left, right, + FelderaBinaryComparisonOperation.FelderaComparisonOperator.getRandom()); + } + + private FelderaExpression generateBooleanExpression(int depth) { + BooleanExpression exprType = Randomly.fromOptions(BooleanExpression.values()); + FelderaExpression expr; + + switch (exprType) { + case NOT: + return new FelderaUnaryPrefixOperation(generateExpression(FelderaSchema.FelderaDataType.BOOLEAN, depth + 1), + FelderaUnaryPrefixOperation.FelderaUnaryPrefixOperator.NOT); + case COMPARISON: + return getBinaryComparison(depth); + case IS_NULL: + return new FelderaUnaryPostfixOperation(generateExpression(getRandomType(), depth + 1), + Randomly.fromOptions(FelderaUnaryPostfixOperation.FelderaUnaryPostfixOperator.IS_NULL, + FelderaUnaryPostfixOperation.FelderaUnaryPostfixOperator.IS_NOT_NULL)); + case BETWEEN: + FelderaSchema.FelderaDataType type = getRandomType(); + expr = generateExpression(type, depth + 1); + FelderaExpression left = generateExpression(type, depth + 1); + FelderaExpression right = generateExpression(type, depth + 1); + return new FelderaBetweenOperation(expr, left, right, + FelderaBetweenOperation.FelderaBetweenOperatorType.getRandom()); + default: + throw new AssertionError(exprType); + } + } + + private FelderaExpression generateStringExpression(int depth) { + FelderaSchema.FelderaDataType type = FelderaSchema.FelderaDataType.VARCHAR; + List applicableFunctions = FelderaFunction.getFunctionCompatibleWith(type); + if (!applicableFunctions.isEmpty()) { + FelderaFunction function = Randomly.fromList(applicableFunctions); + return function.getCall(type, this, depth + 1); + } + + return generateLeafNode(FelderaSchema.FelderaDataType.VARCHAR); + } + + private FelderaExpression generateIntegerString() { + String s = Randomly.StringGenerationStrategy.NUMERIC.getString(globalState.getRandomly()); + if (s.isBlank()) { + s = "1"; + } + return new FelderaConstant.FelderaVarcharConstant(s); + } + + private FelderaExpression getBinaryArithmeticOperation(FelderaSchema.FelderaDataType type, int depth) { + if (Randomly.getBoolean()) { + type = FelderaSchema.FelderaDataType.getRandomNumericType(); + } + return new FelderaBinaryArithmeticOperation(generateExpression(type, depth + 1), + generateExpression(type, depth + 1), + FelderaBinaryArithmeticOperation.FelderaBinaryArithmeticOperator.getRandom()); + } + + @Override + protected FelderaSchema.FelderaDataType getRandomType() { + return FelderaSchema.FelderaDataType.getRandomNonNullType(); + } + + @Override + protected boolean canGenerateColumnOfType(FelderaSchema.FelderaDataType type) { + List columns = filterColumns(type); + return !columns.isEmpty(); + } + + private FelderaExpression getAggregate(FelderaSchema.FelderaDataType type) throws IndexOutOfBoundsException { + FelderaAggregate.FelderaAggregateFunction agg = Randomly + .fromList(FelderaAggregate.FelderaAggregateFunction.getAggregates(type)); + return generateArgsForAggregate(type, agg); + } + + public FelderaExpression generateAggregate() { + while (true) { + try { + return getAggregate(getRandomType()); + } catch (IndexOutOfBoundsException ignored) { + } + } + } + + private FelderaAggregate generateArgsForAggregate(FelderaSchema.FelderaDataType type, + FelderaAggregate.FelderaAggregateFunction agg) { + List types = agg.getTypes(type); + List args = new ArrayList<>(); + allowAggregates = false; + for (FelderaSchema.FelderaDataType argType : types) { + args.add(generateExpression(argType)); + } + + return new FelderaAggregate(agg, args); + + } + + @Override + public FelderaExpression generateExpression(FelderaSchema.FelderaDataType type, int depth) { + if (depth >= maxDepth) { + return generateLeafNode(type); + } + + if (allowAggregates && Randomly.getBoolean() + && !FelderaAggregate.FelderaAggregateFunction.getAggregates(type).isEmpty()) { + return getAggregate(type); + } + + if (Randomly.getBoolean()) { + List applicableFunctions = FelderaFunction.getFunctionCompatibleWith(type); + if (!applicableFunctions.isEmpty()) { + FelderaFunction function = Randomly.fromList(applicableFunctions); + return function.getCall(type, this, depth + 1); + } + } + if (type.isNumeric() && Randomly.getBooleanWithSmallProbability()) { + FelderaSchema.FelderaDataType randomType = FelderaSchema.FelderaDataType.getRandomType(); + FelderaExpression expr; + if (randomType == FelderaSchema.FelderaDataType.VARCHAR) { + expr = generateIntegerString(); + } else if (!randomType.isNumeric()) { + expr = generateExpression(FelderaSchema.FelderaDataType.getRandomNumericType(), depth + 1); + } else { + expr = generateExpression(randomType, depth + 1); + } + return new FelderaCast(expr, type); + } + + switch (type) { + case BOOLEAN: + return generateBooleanExpression(depth); + case VARCHAR: + return generateStringExpression(depth); + case TINYINT: + case SMALLINT: + case INT: + case BIGINT: + case REAL: + case DOUBLE: + return getBinaryArithmeticOperation(type, depth); + case DATE: + case TIMESTAMP: + case TIME: + case CHAR: + FelderaExpression expr = FelderaConstant.getRandomConstant(globalState, type); + return new FelderaCast(expr, type); + default: + throw new AssertionError(type); + } + } + + List filterColumns(FelderaSchema.FelderaDataType type) { + if (columns == null) { + return Collections.emptyList(); + } else { + return columns.stream().filter(c -> c.getType() == type).collect(Collectors.toList()); + } + } + + @Override + protected FelderaExpression generateColumn(FelderaSchema.FelderaDataType type) { + // HACK: if no col of such type exists, generate constant value instead + List colsOfType = filterColumns(type); + if (colsOfType.isEmpty()) { + return generateConstant(type); + } + + FelderaSchema.FelderaColumn column = Randomly.fromList(colsOfType); + return new FelderaColumnReference(column); + } + + @Override + public FelderaExpression generateConstant(FelderaSchema.FelderaDataType type) { + return type.getRandomConstant(globalState); + } + + @Override + public FelderaExpression generatePredicate() { + return generateExpression(FelderaSchema.FelderaDataType.BOOLEAN, 0); + } + + @Override + public FelderaExpression negatePredicate(FelderaExpression predicate) { + return new FelderaUnaryPrefixOperation(predicate, FelderaUnaryPrefixOperation.FelderaUnaryPrefixOperator.NOT); + } + + @Override + public FelderaExpression isNull(FelderaExpression expr) { + return new FelderaUnaryPostfixOperation(expr, FelderaUnaryPostfixOperation.FelderaUnaryPostfixOperator.IS_NULL); + } + + @Override + public FelderaExpressionGenerator setTablesAndColumns( + AbstractTables tables) { + this.tables = tables.getTables(); + this.columns = tables.getColumns(); + + return this; + } + + @Override + public FelderaExpression generateBooleanExpression() { + return generateExpression(FelderaSchema.FelderaDataType.BOOLEAN); + } + + @Override + public FelderaSelect generateSelect() { + FelderaSelect select = new FelderaSelect(); + select.setDistinct(Randomly.getBooleanWithSmallProbability()); + + List columns = new ArrayList<>(); + + int nrColumns = Randomly.smallNumber() + 1; + + for (int i = 0; i < nrColumns; i++) { + if (allowAggregates && Randomly.getBoolean()) { + FelderaExpression expression = generateExpression(getRandomType()); + columns.add(expression); + } else { + columns.add(generateAggregate()); + } + } + + select.setFetchColumns(columns); + List randomJoins = getRandomJoinClauses(); + List joinedTables = new ArrayList<>(); + + for (FelderaJoin join : randomJoins) { + joinedTables.add(join.getLeftTable()); + joinedTables.add(join.getRightTable()); + } + + select.setJoinClauses(randomJoins); + + List fromList = getTableRefs().stream() + .filter(t -> !joinedTables.contains((FelderaTableReference) t)).collect(Collectors.toList()); + select.setFromList(fromList); + + return select; + } + + @Override + public List getRandomJoinClauses() { + return getRandomJoinClauses(new ArrayList<>(tables)); + } + + public List getRandomJoinClauses(List tables) { + List tablesRef = tables.stream().map(FelderaTableReference::new) + .collect(Collectors.toList()); + List joinStatements = new ArrayList<>(); + List options = new ArrayList<>( + Arrays.asList(FelderaJoin.FelderaJoinType.values())); + + if (tablesRef.size() >= 2) { + int nrJoinClauses = (int) Randomly.getNotCachedInteger(0, (tables.size() / 2) - 1); + // natural join is incompatible with other joins + // because it needs unique column names + // while other joins will produce duplicate column names + if (nrJoinClauses > 1) { + options.remove(FelderaJoin.FelderaJoinType.NATURAL); + } + for (int i = 0; i < nrJoinClauses; i++) { + FelderaExpression joinClause = generatePredicate(); + FelderaTableReference leftTable = Randomly.fromList(tablesRef); + tablesRef.remove(leftTable); + FelderaTableReference rightTable = Randomly.fromList(tablesRef); + tablesRef.remove(rightTable); + + FelderaJoin.FelderaJoinType selectedOption = Randomly.fromList(options); + if (selectedOption == FelderaJoin.FelderaJoinType.NATURAL) { + // NATURAL joins do not have an ON clause + joinClause = null; + } + + FelderaJoin j = new FelderaJoin(leftTable, rightTable, selectedOption, joinClause); + joinStatements.add(j); + } + } + + return joinStatements; + } + + @Override + public List getTableRefs() { + return tables.stream().map(FelderaTableReference::new).collect(Collectors.toList()); + } + + @Override + public String generateOptimizedQueryString(FelderaSelect select, FelderaExpression whereCondition, + boolean shouldUseAggregate) { + select.setWhereClause(whereCondition); + + return select.asString(); + } + + @Override + public String generateUnoptimizedQueryString(FelderaSelect select, FelderaExpression whereCondition) { + whereCondition.setBlackbox(true); + select.getFetchColumns().forEach(c -> { + c.setBlackbox(true); + }); + select.setWhereClause(whereCondition); + + return select.asString(); + } +} \ No newline at end of file diff --git a/src/sqlancer/feldera/gen/FelderaInsertGenerator.java b/src/sqlancer/feldera/gen/FelderaInsertGenerator.java new file mode 100644 index 000000000..22e3eb94f --- /dev/null +++ b/src/sqlancer/feldera/gen/FelderaInsertGenerator.java @@ -0,0 +1,47 @@ +package sqlancer.feldera.gen; + +import sqlancer.common.gen.AbstractInsertGenerator; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.schema.AbstractTableColumn; +import sqlancer.feldera.FelderaGlobalState; +import sqlancer.feldera.FelderaSchema; +import sqlancer.feldera.FelderaToStringVisitor; +import sqlancer.feldera.query.FelderaOtherQuery; + +import java.util.List; +import java.util.stream.Collectors; + +public class FelderaInsertGenerator extends AbstractInsertGenerator { + private final FelderaGlobalState globalState; + private final ExpectedErrors errors = new ExpectedErrors(); + + private FelderaInsertGenerator(FelderaGlobalState globalState) { + this.globalState = globalState; + } + + public static FelderaOtherQuery getQuery(FelderaGlobalState globalState) { + return new FelderaInsertGenerator(globalState).generate(); + } + + private FelderaOtherQuery generate() { + sb.append("INSERT INTO "); + FelderaSchema.FelderaTable table = globalState.getSchema().getRandomTable(t -> !t.isView()); + List columns = table + .getRandomNonEmptyColumnSubsetFilter(p -> !p.getName().equals("rowid")); + sb.append(table.getName()); + sb.append("("); + sb.append(columns.stream().map(AbstractTableColumn::getName).collect(Collectors.joining(", "))); + sb.append(")"); + sb.append(" VALUES "); + insertColumns(columns); + sb.append(";"); + String s = sb.toString(); + return new FelderaOtherQuery(s, errors); + } + + @Override + protected void insertValue(FelderaSchema.FelderaColumn column) { + sb.append(FelderaToStringVisitor + .asString(new FelderaExpressionGenerator(globalState).generateConstant(column.getType()))); + } +} diff --git a/src/sqlancer/feldera/gen/FelderaTableGenerator.java b/src/sqlancer/feldera/gen/FelderaTableGenerator.java new file mode 100644 index 000000000..419471c00 --- /dev/null +++ b/src/sqlancer/feldera/gen/FelderaTableGenerator.java @@ -0,0 +1,55 @@ +package sqlancer.feldera.gen; + +import sqlancer.Randomly; +import sqlancer.common.DBMSCommon; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.feldera.FelderaSchema; +import sqlancer.feldera.query.FelderaOtherQuery; + +import java.util.ArrayList; +import java.util.List; + +public class FelderaTableGenerator { + protected final ExpectedErrors errors = new ExpectedErrors(); + private final String tableName; + private final StringBuilder sb = new StringBuilder(); + private final List columnsToBeAdded = new ArrayList<>(); + private FelderaSchema.FelderaTable table; + + public FelderaTableGenerator(String tableName) { + this.tableName = tableName; + } + + public FelderaOtherQuery generate() { + table = new FelderaSchema.FelderaTable(tableName, columnsToBeAdded); + + sb.append("CREATE TABLE "); + sb.append(tableName); + sb.append("("); + + int nrColumns = Randomly.smallNumber() + 1; + for (int i = 0; i < nrColumns; i++) { + if (i != 0) { + sb.append(", "); + } + createField(DBMSCommon.createColumnName(i)); + } + + sb.append(") with ('materialized' = 'true');\n"); + return new FelderaOtherQuery(sb.toString(), new ExpectedErrors()); + } + + private void createField(String name) throws AssertionError { + sb.append(name); + sb.append(" "); + FelderaSchema.FelderaDataType type = FelderaSchema.FelderaDataType.getRandomNonNullType(); + FelderaCommon.appendDataType(type, sb); + FelderaSchema.FelderaFieldColumn c = new FelderaSchema.FelderaFieldColumn(name, type); + c.setTable(table); + columnsToBeAdded.add(c); + } + + public FelderaSchema.FelderaTable getTable() { + return this.table; + } +} diff --git a/src/sqlancer/feldera/gen/FelderaViewGenerator.java b/src/sqlancer/feldera/gen/FelderaViewGenerator.java new file mode 100644 index 000000000..40aee2ef9 --- /dev/null +++ b/src/sqlancer/feldera/gen/FelderaViewGenerator.java @@ -0,0 +1,51 @@ +package sqlancer.feldera.gen; + +import sqlancer.Randomly; +import sqlancer.common.oracle.TestOracleUtils; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.feldera.FelderaGlobalState; +import sqlancer.feldera.ast.FelderaExpression; +import sqlancer.feldera.ast.FelderaSelect; +import sqlancer.feldera.query.FelderaOtherQuery; + +import java.util.ArrayList; +import java.util.List; + +public class FelderaViewGenerator { + public FelderaViewGenerator() { + } + + public static List generate(FelderaGlobalState globalState, String viewName) { + List queries = new ArrayList<>(); + FelderaExpressionGenerator gen = new FelderaExpressionGenerator(globalState) + .setTablesAndColumns(TestOracleUtils.getRandomTableNonEmptyTables(globalState.getSchema())); + FelderaSelect select = gen.generateSelect(); + FelderaExpression whereCondition = gen.generateBooleanExpression(); + + queries.add(generateViewFromSelect(select, gen, whereCondition, viewName, true)); + queries.add(generateViewFromSelect(select, gen, whereCondition, viewName, false)); + return queries; + } + + private static FelderaOtherQuery generateViewFromSelect(FelderaSelect select, FelderaExpressionGenerator gen, + FelderaExpression whereCondition, String viewName, boolean optimized) { + ExpectedErrors errors = new ExpectedErrors(); + StringBuilder sb = new StringBuilder("CREATE MATERIALIZED VIEW "); + sb.append(viewName); + if (optimized) { + sb.append("_optimized"); + } + + sb.append(" AS ("); + String selectQuery; + + if (optimized) { + selectQuery = gen.generateOptimizedQueryString(select, whereCondition, Randomly.getBoolean()); + } else { + selectQuery = gen.generateUnoptimizedQueryString(select, whereCondition); + } + sb.append(selectQuery); + sb.append(");\n"); + return new FelderaOtherQuery(sb.toString(), errors); + } +} diff --git a/src/sqlancer/feldera/oracle/FelderaNoRECOracle.java b/src/sqlancer/feldera/oracle/FelderaNoRECOracle.java new file mode 100644 index 000000000..8cf601790 --- /dev/null +++ b/src/sqlancer/feldera/oracle/FelderaNoRECOracle.java @@ -0,0 +1,36 @@ +package sqlancer.feldera.oracle; + +import sqlancer.Main; +import sqlancer.MainOptions; +import sqlancer.common.oracle.TestOracle; +import sqlancer.feldera.FelderaConnection; +import sqlancer.feldera.FelderaGlobalState; + +import java.util.Map; + +public class FelderaNoRECOracle implements TestOracle { + + protected final FelderaGlobalState state; + protected final Main.StateLogger logger; + protected final MainOptions options; + protected final FelderaConnection con; + + public FelderaNoRECOracle(FelderaGlobalState state) { + this.state = state; + this.con = state.getConnection(); + this.logger = state.getLogger(); + this.options = state.getOptions(); + } + + @Override + public void check() throws Exception { + con.prepare(); + for (String view : state.getViews()) { + String query = String.format("select * from %s except select * from %s_optimized", view, view); + Map ret = con.execute(query); + if (!ret.isEmpty()) { + throw new AssertionError("query failed: " + query); + } + } + } +} diff --git a/src/sqlancer/feldera/query/FelderaOtherQuery.java b/src/sqlancer/feldera/query/FelderaOtherQuery.java new file mode 100644 index 000000000..0d1740520 --- /dev/null +++ b/src/sqlancer/feldera/query/FelderaOtherQuery.java @@ -0,0 +1,30 @@ +package sqlancer.feldera.query; + +import sqlancer.GlobalState; +import sqlancer.IgnoreMeException; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.feldera.FelderaConnection; + +public class FelderaOtherQuery extends FelderaQueryAdapter { + public FelderaOtherQuery(String query, ExpectedErrors errors) { + super(query, errors); + } + + @Override + public boolean couldAffectSchema() { + return true; + } + + @Override + public > boolean execute(G globalState, String... fills) + throws Exception { + try { + globalState.getConnection().buffer(query); + } catch (Exception e) { + if (this.errors.errorIsExpected(e.getMessage())) { + throw new IgnoreMeException(); + } + } + return true; + } +} diff --git a/src/sqlancer/feldera/query/FelderaQueryAdapter.java b/src/sqlancer/feldera/query/FelderaQueryAdapter.java new file mode 100644 index 000000000..0858e2d13 --- /dev/null +++ b/src/sqlancer/feldera/query/FelderaQueryAdapter.java @@ -0,0 +1,41 @@ +package sqlancer.feldera.query; + +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.query.Query; +import sqlancer.feldera.FelderaConnection; + +public abstract class FelderaQueryAdapter extends Query { + + String query; + ExpectedErrors errors; + + public FelderaQueryAdapter(String query, ExpectedErrors errors) { + this.query = query; + this.errors = errors; + } + + @Override + public String getLogString() { + return query; + } + + @Override + public String getQueryString() { + return query; + } + + @Override + public String getUnterminatedQueryString() { + return null; + } + + @Override + public boolean couldAffectSchema() { + return false; + } + + @Override + public ExpectedErrors getExpectedErrors() { + return errors; + } +} diff --git a/src/sqlancer/feldera/query/FelderaQueryProvider.java b/src/sqlancer/feldera/query/FelderaQueryProvider.java new file mode 100644 index 000000000..c886544a0 --- /dev/null +++ b/src/sqlancer/feldera/query/FelderaQueryProvider.java @@ -0,0 +1,6 @@ +package sqlancer.feldera.query; + +@FunctionalInterface +public interface FelderaQueryProvider { + FelderaOtherQuery getQuery(S globalState) throws Exception; +}