-
Notifications
You must be signed in to change notification settings - Fork 398
Add initial QuestDB support with simple SELECT generator (Fixes #457) #1277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,14 +13,23 @@ private QuestDBErrors() { | |
| public static List<String> getExpressionErrors() { | ||
| ArrayList<String> errors = new ArrayList<>(); | ||
|
|
||
| // TODO (anxing) | ||
| // QuestDB-specific expression errors | ||
| errors.add("unexpected argument for function: "); | ||
| errors.add("unexpected token:"); // SELECT FROM multiple tables without WHERE/ JOIN clause | ||
| errors.add("boolean expression expected"); | ||
| errors.add("Column name expected"); | ||
| errors.add("too few arguments for 'in'"); | ||
| errors.add("cannot compare TIMESTAMP with type"); // WHERE column IN with nonTIMESTAMP arg | ||
| errors.add("constant expected"); | ||
| errors.add("invalid column reference"); | ||
| errors.add("syntax error"); | ||
| errors.add("unexpected end of statement"); | ||
| errors.add("invalid operator"); | ||
| errors.add("type mismatch"); | ||
| errors.add("division by zero"); | ||
| errors.add("invalid function call"); | ||
| errors.add("missing FROM clause"); | ||
| errors.add("duplicate column name"); | ||
|
|
||
| return errors; | ||
| } | ||
|
|
@@ -30,9 +39,14 @@ public static void addExpressionErrors(ExpectedErrors errors) { | |
| } | ||
|
|
||
| public static List<String> getGroupByErrors() { | ||
| // TODO (anxing) | ||
|
|
||
| return new ArrayList<>(); | ||
| ArrayList<String> errors = new ArrayList<>(); | ||
|
|
||
| // QuestDB-specific GROUP BY errors | ||
| errors.add("GROUP BY expression must be in SELECT list"); | ||
| errors.add("aggregate function not allowed in GROUP BY"); | ||
| errors.add("column must appear in GROUP BY clause"); | ||
|
|
||
| return errors; | ||
| } | ||
|
|
||
| public static void addGroupByErrors(ExpectedErrors errors) { | ||
|
|
@@ -42,15 +56,30 @@ public static void addGroupByErrors(ExpectedErrors errors) { | |
| public static List<String> getInsertErrors() { | ||
| ArrayList<String> errors = new ArrayList<>(); | ||
|
|
||
| // TODO (anxing) | ||
| // QuestDB-specific insert errors | ||
| errors.add("Invalid column"); | ||
| errors.add("inconvertible types:"); | ||
| errors.add("inconvertible value:"); | ||
| errors.add("column count mismatch"); | ||
Swastik092 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| errors.add("duplicate key value"); | ||
| errors.add("constraint violation"); | ||
| errors.add("invalid data type"); | ||
| errors.add("value too large"); | ||
| errors.add("missing required column"); | ||
| errors.add("table does not exist"); | ||
| errors.add("permission denied"); | ||
| errors.add("syntax error in INSERT statement"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Syntax errors are always unexpected, so we should fix this in the generator. |
||
|
|
||
| return errors; | ||
| } | ||
|
|
||
| public static void addInsertErrors(ExpectedErrors errors) { | ||
| errors.addAll(getInsertErrors()); | ||
| } | ||
|
|
||
| public static void addAllErrors(ExpectedErrors errors) { | ||
| addExpressionErrors(errors); | ||
| addGroupByErrors(errors); | ||
| addInsertErrors(errors); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,20 +23,22 @@ | |
| import sqlancer.questdb.gen.QuestDBInsertGenerator; | ||
| import sqlancer.questdb.gen.QuestDBTableGenerator; | ||
| import sqlancer.questdb.gen.QuestDBTruncateGenerator; | ||
| import sqlancer.questdb.gen.QuestDBUpdateGenerator; | ||
| import sqlancer.questdb.gen.QuestDBViewGenerator; | ||
|
|
||
| @AutoService(DatabaseProvider.class) | ||
| public class QuestDBProvider extends SQLProviderAdapter<QuestDBGlobalState, QuestDBOptions> { | ||
| public QuestDBProvider() { | ||
| super(QuestDBGlobalState.class, QuestDBOptions.class); | ||
| } | ||
|
|
||
|
|
||
| public enum Action implements AbstractAction<QuestDBGlobalState> { | ||
| INSERT(QuestDBInsertGenerator::getQuery), // | ||
| ALTER_INDEX(QuestDBAlterIndexGenerator::getQuery), // | ||
| TRUNCATE(QuestDBTruncateGenerator::generate); // | ||
| // TODO (anxing): maybe implement these later | ||
| // UPDATE(QuestDBUpdateGenerator::getQuery), // | ||
| // CREATE_VIEW(QuestDBViewGenerator::generate), // | ||
| TRUNCATE(QuestDBTruncateGenerator::generate), // | ||
| UPDATE(QuestDBUpdateGenerator::getQuery), // | ||
| CREATE_VIEW(QuestDBViewGenerator::generate); // | ||
|
|
||
| private final SQLQueryProvider<QuestDBGlobalState> sqlQueryProvider; | ||
|
|
||
|
|
@@ -59,6 +61,10 @@ private static int mapActions(QuestDBGlobalState globalState, Action a) { | |
| return r.getInteger(0, 3); | ||
| case TRUNCATE: | ||
| return r.getInteger(0, 5); | ||
| case UPDATE: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should only add new features once we have a CI/CD that works for a given QuestDB version. |
||
| return r.getInteger(0, 3); | ||
| case CREATE_VIEW: | ||
| return r.getInteger(0, 2); | ||
| default: | ||
| throw new AssertionError("Unknown action: " + a); | ||
| } | ||
|
|
@@ -72,6 +78,7 @@ protected QuestDBSchema readSchema() throws SQLException { | |
| } | ||
|
|
||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public void generateDatabase(QuestDBGlobalState globalState) throws Exception { | ||
|
|
@@ -104,47 +111,47 @@ public SQLConnection createDatabase(QuestDBGlobalState globalState) throws Excep | |
| if (port == sqlancer.MainOptions.NO_SET_PORT) { | ||
| port = QuestDBOptions.DEFAULT_PORT; | ||
| } | ||
| // TODO(anxing): maybe not hardcode here... | ||
|
|
||
| String databaseName = "qdb"; | ||
| String tableName = "sqlancer_test"; | ||
| String url = String.format("jdbc:postgresql://%s:%d/%s", host, port, databaseName); | ||
| // use QuestDB default username & password for Postgres JDBC | ||
|
|
||
| // Use QuestDB default username & password for Postgres JDBC | ||
| Properties properties = new Properties(); | ||
| properties.setProperty("user", globalState.getDbmsSpecificOptions().getUserName()); | ||
| properties.setProperty("password", globalState.getDbmsSpecificOptions().getPassword()); | ||
| properties.setProperty("sslmode", "disable"); | ||
|
|
||
| Connection con = DriverManager.getConnection(url, properties); | ||
| // QuestDB cannot create or drop `DATABASE`, can only create or drop `TABLE` | ||
| globalState.getState().logStatement("DROP TABLE IF EXISTS " + tableName + " CASCADE"); | ||
|
|
||
| // QuestDB cannot create or drop DATABASE, can only create or drop TABLE | ||
| // Clean up any existing test tables | ||
| cleanupTestTables(con); | ||
|
|
||
| // Create a test table to verify connection | ||
| String tableName = "sqlancer_test"; | ||
| SQLQueryAdapter createTableCommand = new QuestDBTableGenerator().getQuery(globalState, tableName); | ||
| globalState.getState().logStatement(createTableCommand); | ||
| globalState.getState().logStatement("DROP TABLE IF EXISTS " + tableName); | ||
|
|
||
| try (Statement s = con.createStatement()) { | ||
| s.execute("DROP TABLE IF EXISTS " + tableName); | ||
| } | ||
| // TODO(anxing): Drop all previous tables in db | ||
| // List<String> tableNames = | ||
| // globalState.getSchema().getDatabaseTables().stream().map(AbstractTable::getName).collect(Collectors.toList()); | ||
| // for (String tName : tableNames) { | ||
| // try (Statement s = con.createStatement()) { | ||
| // String query = "DROP TABLE IF EXISTS " + tName; | ||
| // globalState.getState().logStatement(query); | ||
| // s.execute(query); | ||
| // } | ||
| // } | ||
|
|
||
| try (Statement s = con.createStatement()) { | ||
| s.execute(createTableCommand.getQueryString()); | ||
| } | ||
| // drop test table | ||
|
|
||
| // Drop the test table after creation | ||
| try (Statement s = con.createStatement()) { | ||
| s.execute("DROP TABLE IF EXISTS " + tableName); | ||
| } | ||
|
|
||
| con.close(); | ||
| con = DriverManager.getConnection(url, properties); | ||
| return new SQLConnection(con); | ||
| } | ||
|
|
||
| private void cleanupTestTables(Connection con) throws SQLException { | ||
| // Clean up any existing tables that might interfere with testing | ||
| try (Statement s = con.createStatement()) { | ||
| s.execute("DROP TABLE IF EXISTS sqlancer_test CASCADE"); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public String getDBMSName() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package sqlancer.questdb; | ||
|
|
||
| import sqlancer.common.query.ExpectedErrors; | ||
| import sqlancer.common.query.SQLQueryAdapter; | ||
| import java.util.List; | ||
|
|
||
| public class QuestDBQueryGenerator { | ||
|
|
||
| // Generate a simple SELECT query for QuestDB | ||
| public SQLQueryAdapter generateSelect(String tableName, List<String> columns) { | ||
| StringBuilder sb = new StringBuilder(); | ||
| ExpectedErrors errors = new ExpectedErrors(); | ||
|
|
||
| sb.append("SELECT "); | ||
| if (columns == null || columns.isEmpty()) { | ||
| sb.append("*"); | ||
| } else { | ||
| for (int i = 0; i < columns.size(); i++) { | ||
| sb.append(columns.get(i)); | ||
| if (i < columns.size() - 1) { | ||
| sb.append(", "); | ||
| } | ||
| } | ||
| } | ||
| sb.append(" FROM ").append(tableName); | ||
| sb.append(" LIMIT 10"); // simple query without semicolon for better compatibility | ||
|
|
||
| // Add QuestDB-specific errors | ||
| QuestDBErrors.addExpressionErrors(errors); | ||
|
|
||
| return new SQLQueryAdapter(sb.toString(), errors); | ||
| } | ||
|
|
||
| // Generate a simple SELECT query with WHERE clause | ||
| public SQLQueryAdapter generateSelectWithWhere(String tableName, List<String> columns, String whereClause) { | ||
| StringBuilder sb = new StringBuilder(); | ||
| ExpectedErrors errors = new ExpectedErrors(); | ||
|
|
||
| sb.append("SELECT "); | ||
| if (columns == null || columns.isEmpty()) { | ||
| sb.append("*"); | ||
| } else { | ||
| for (int i = 0; i < columns.size(); i++) { | ||
| sb.append(columns.get(i)); | ||
| if (i < columns.size() - 1) { | ||
| sb.append(", "); | ||
| } | ||
| } | ||
| } | ||
| sb.append(" FROM ").append(tableName); | ||
|
|
||
| if (whereClause != null && !whereClause.trim().isEmpty()) { | ||
| sb.append(" WHERE ").append(whereClause); | ||
| } | ||
|
|
||
| sb.append(" LIMIT 10"); | ||
|
|
||
| // Add QuestDB-specific errors | ||
| QuestDBErrors.addExpressionErrors(errors); | ||
|
|
||
| return new SQLQueryAdapter(sb.toString(), errors); | ||
| } | ||
| } |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like an accidental commit. |
Uh oh!
There was an error while loading. Please reload this page.