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;
+}