From 05e563d5a8b0cba1287e958459db3118ef6783be Mon Sep 17 00:00:00 2001 From: kenstott <128912107+kenstott@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:53:47 -0500 Subject: [PATCH] Fixed date handling for calcite graphql adapter. Added a sqlengine - for http sql api. Made query parsing identifiers case-sensitive to match PostgresSQL style. --- calcite-rs-jni/jdbc/pom.xml | 256 ++++++++++++++---- .../main/java/com/hasura/GraphQLDriver.java | 3 + .../java/com/hasura/CalciteModelPlanner.java | 14 +- .../main/java/com/hasura/CalciteQuery.java | 3 + calcite-rs-jni/pom.xml | 1 + calcite-rs-jni/sqlengine/pom.xml | 34 +++ .../main/java/com/hasura/SQLHttpServer.java | 162 +++++++++++ 7 files changed, 421 insertions(+), 52 deletions(-) create mode 100644 calcite-rs-jni/sqlengine/pom.xml create mode 100644 calcite-rs-jni/sqlengine/src/main/java/com/hasura/SQLHttpServer.java diff --git a/calcite-rs-jni/jdbc/pom.xml b/calcite-rs-jni/jdbc/pom.xml index 2d4bd31..b95e324 100644 --- a/calcite-rs-jni/jdbc/pom.xml +++ b/calcite-rs-jni/jdbc/pom.xml @@ -8,97 +8,235 @@ ndc-calcite 1.0.0 + graphql-jdbc-driver jar + UTF-8 11 11 1.38.0-SNAPSHOT 1.0.0 + + + 1.9.10 + 2.15.2 + 1.7.36 + 3.11 + 1.0.3 + 5.2.3 + 1.25.0 + 22.3 + 11.0.1 + 4.12.0 + 1.15 + 3.6.1 + 1.9 + 33.0.0-jre + 3.1.6 + 2.3.0 + 1.18.1 + 2.14.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib-common + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib-jdk7 + ${kotlin.version} + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + + org.reactivestreams + reactive-streams + ${reactive-streams.version} + + + + + org.apache.httpcomponents.core5 + httpcore5 + ${httpcore5.version} + + + + + - org.apache.calcite.avatica - avatica-core - 1.25.0 + org.apache.calcite + calcite-core + ${calcite.version} + + org.apache.calcite + calcite-graphql + ${calcite.version} + + + org.apache.calcite + calcite-linq4j + ${calcite.version} + + + com.graphql-java graphql-java - 22.3 + ${graphql-java.version} + + + org.reactivestreams + reactive-streams + + com.graphql-java-kickstart graphql-java-tools - 11.0.1 + ${graphql-java-tools.version} + + + com.graphql-java + graphql-java + + + org.jetbrains.kotlin + kotlin-stdlib + + + com.fasterxml.jackson.core + * + + + org.apache.commons + commons-lang3 + + + + com.squareup.okhttp3 okhttp - 4.12.0 - - - commons-codec - commons-codec - 1.15 - - - com.jayway.jsonpath - json-path - 2.3.0 - - - com.google.guava - guava - 33.0.0-jre + ${okhttp.version} + + - org.locationtech.jts - jts-core - 1.18.1 + org.apache.calcite.avatica + avatica-core + ${avatica.version} + + - org.codehaus.janino - janino - 3.1.6 + commons-codec + commons-codec + ${commons-codec.version} + runtime org.apache.commons commons-math3 - 3.6.1 + ${commons-math.version} + runtime org.apache.commons commons-text - 1.9 + ${commons-text.version} + runtime - org.apache.calcite - calcite-core - ${calcite.version} + com.google.guava + guava + ${guava.version} + runtime - org.apache.calcite - calcite-graphql - ${calcite.version} + org.codehaus.janino + janino + ${janino.version} + runtime - org.apache.calcite - calcite-linq4j - ${calcite.version} + com.jayway.jsonpath + json-path + ${json-path.version} + runtime + + org.locationtech.jts + jts-core + ${jts.version} + runtime + + + org.apache.logging.log4j log4j-api - 2.14.1 + ${log4j.version} org.apache.logging.log4j log4j-core - 2.14.1 + ${log4j.version} + runtime + + junit junit @@ -106,6 +244,7 @@ test + @@ -114,6 +253,29 @@ + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-versions + + enforce + + + + + + + true + + + + + + org.codehaus.mojo exec-maven-plugin @@ -175,19 +337,11 @@ - - org.apache.maven.plugins - maven-jar-plugin - 3.2.0 - - - false - - - + + maven-assembly-plugin - 3.6.0 + 3.7.1 jar-with-dependencies diff --git a/calcite-rs-jni/jdbc/src/main/java/com/hasura/GraphQLDriver.java b/calcite-rs-jni/jdbc/src/main/java/com/hasura/GraphQLDriver.java index b33e858..7882fe4 100644 --- a/calcite-rs-jni/jdbc/src/main/java/com/hasura/GraphQLDriver.java +++ b/calcite-rs-jni/jdbc/src/main/java/com/hasura/GraphQLDriver.java @@ -70,6 +70,9 @@ public Connection connect(String url, Properties info) throws SQLException { Properties calciteProps = new Properties(); calciteProps.setProperty("fun", "standard"); + calciteProps.setProperty("caseSensitive", "true"); + calciteProps.setProperty("unquotedCasing", "UNCHANGED"); + calciteProps.setProperty("quotedCasing", "UNCHANGED"); Connection connection = DriverManager.getConnection("jdbc:calcite:", calciteProps); CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class); diff --git a/calcite-rs-jni/jni/src/main/java/com/hasura/CalciteModelPlanner.java b/calcite-rs-jni/jni/src/main/java/com/hasura/CalciteModelPlanner.java index e782952..3ac3374 100644 --- a/calcite-rs-jni/jni/src/main/java/com/hasura/CalciteModelPlanner.java +++ b/calcite-rs-jni/jni/src/main/java/com/hasura/CalciteModelPlanner.java @@ -3,6 +3,7 @@ import org.apache.calcite.adapter.enumerable.EnumerableConvention; import org.apache.calcite.adapter.enumerable.EnumerableRules; import org.apache.calcite.adapter.graphql.GraphQLRules; +import org.apache.calcite.avatica.util.Casing; import org.apache.calcite.jdbc.CalciteConnection; import org.apache.calcite.plan.RelOptPlanner; import org.apache.calcite.plan.RelOptRule; @@ -16,6 +17,7 @@ import org.apache.calcite.schema.SchemaPlus; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.parser.SqlParser; +import org.apache.calcite.sql.validate.SqlConformanceEnum; import org.apache.calcite.tools.*; import java.sql.Connection; @@ -38,6 +40,9 @@ public class CalciteModelPlanner { public static void displayQueryPlan(String modelPath, String sql) throws Exception { Properties info = new Properties(); info.put("model", modelPath); + info.setProperty("caseSensitive", "true"); + info.setProperty("unquotedCasing", "UNCHANGED"); + info.setProperty("quotedCasing", "UNCHANGED"); Connection connection = DriverManager.getConnection("jdbc:calcite:", info); CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class); @@ -72,10 +77,17 @@ public static void displayQueryPlan(String modelPath, String sql) throws Excepti hepProgramBuilder.addMatchOrder(HepMatchOrder.TOP_DOWN); rules.forEach(hepProgramBuilder::addRuleInstance); + // Set the SQL parser configuration + SqlParser.Config parserConfig = SqlParser.config() + .withCaseSensitive(true) // For distinguishing between "name" and "Name" + .withUnquotedCasing(Casing.UNCHANGED) // Keep original case for unquoted identifiers + .withQuotedCasing(Casing.UNCHANGED) // Keep original case for quoted identifiers + .withConformance(SqlConformanceEnum.LENIENT); + // Create planner configuration FrameworkConfig config = Frameworks.newConfigBuilder() .defaultSchema(rootSchema) - .parserConfig(SqlParser.Config.DEFAULT) + .parserConfig(parserConfig) .programs(Programs.sequence( Programs.ofRules(rules), Programs.ofRules(GraphQLRules.PROJECT_RULE), diff --git a/calcite-rs-jni/jni/src/main/java/com/hasura/CalciteQuery.java b/calcite-rs-jni/jni/src/main/java/com/hasura/CalciteQuery.java index 75f7dba..45f7337 100644 --- a/calcite-rs-jni/jni/src/main/java/com/hasura/CalciteQuery.java +++ b/calcite-rs-jni/jni/src/main/java/com/hasura/CalciteQuery.java @@ -136,6 +136,9 @@ public Connection createCalciteConnection(String modelPath) throws IOException { span.setAttribute("modelPath", modelPath); Properties info = new Properties(); info.setProperty("model", ConfigPreprocessor.preprocessConfig(modelPath)); + info.setProperty("caseSensitive", "true"); + info.setProperty("unquotedCasing", "UNCHANGED"); + info.setProperty("quotedCasing", "UNCHANGED"); try { // Class.forName("com.simba.googlebigquery.jdbc42.Driver"); Class.forName("org.apache.calcite.jdbc.Driver"); diff --git a/calcite-rs-jni/pom.xml b/calcite-rs-jni/pom.xml index 01b1b85..efe6504 100644 --- a/calcite-rs-jni/pom.xml +++ b/calcite-rs-jni/pom.xml @@ -23,6 +23,7 @@ ./bigquery ./calcite ./jdbc + sqlengine diff --git a/calcite-rs-jni/sqlengine/pom.xml b/calcite-rs-jni/sqlengine/pom.xml new file mode 100644 index 0000000..8a394b3 --- /dev/null +++ b/calcite-rs-jni/sqlengine/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + com.hasura + ndc-calcite + 1.0.0 + + + + org.json + json + 20231013 + + + com.hasura + graphql-jdbc-driver + 1.0.0 + system + ${project.basedir}/../jdbc/target/graphql-jdbc-driver-1.0.0-jar-with-dependencies.jar + + + + sqlengine + + + 11 + 11 + UTF-8 + + + \ No newline at end of file diff --git a/calcite-rs-jni/sqlengine/src/main/java/com/hasura/SQLHttpServer.java b/calcite-rs-jni/sqlengine/src/main/java/com/hasura/SQLHttpServer.java new file mode 100644 index 0000000..29084fe --- /dev/null +++ b/calcite-rs-jni/sqlengine/src/main/java/com/hasura/SQLHttpServer.java @@ -0,0 +1,162 @@ +package com.hasura; + +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import org.json.JSONObject; +import org.json.JSONArray; + +import java.io.*; +import java.net.InetSocketAddress; +import java.sql.*; +import java.util.*; +import java.nio.charset.StandardCharsets; + +public class SQLHttpServer { + private static final String JDBC_URL = System.getenv("JDBC_URL"); + private static final int PORT = getPortFromEnv(); + + static { + try { + Class.forName("com.hasura.GraphQLDriver"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private static int getPortFromEnv() { + String portStr = System.getenv("PORT"); + if (portStr != null) { + try { + return Integer.parseInt(portStr); + } catch (NumberFormatException e) { + // ignore + } + } + System.err.println("Warning: Invalid PORT environment variable. Using default port 8080"); + return 8080; + } + + public static void main(String[] args) throws IOException { + // Validate environment variables + if (JDBC_URL == null) { + System.err.println("Error: Required environment variable JDBC_URL must be set"); + System.exit(1); + } + + HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0); + server.createContext("/sql", new SQLHandler()); + server.setExecutor(null); + server.start(); + System.out.println("Server started on port " + PORT); + } + + static class SQLHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!exchange.getRequestMethod().equals("POST")) { + sendResponse(exchange, 405, "Method not allowed"); + return; + } + + // Extract connection properties from headers + Properties connectionProps = new Properties(); + + // Get user from X-Hasura-User header + String user = exchange.getRequestHeaders().getFirst("X-Hasura-User"); + if (user != null) { + connectionProps.setProperty("user", user); + } + + // Get role from X-Hasura-Role header + String role = exchange.getRequestHeaders().getFirst("X-Hasura-Role"); + if (role != null) { + connectionProps.setProperty("role", role); + } + + // Get auth from Authorization header + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + if (auth != null) { + connectionProps.setProperty("auth", auth); + } + + // Get password from password header + String password = exchange.getRequestHeaders().getFirst("Password"); + if (password != null) { + connectionProps.setProperty("password", password); + } + + try { + // Read request body + String requestBody = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + JSONObject jsonRequest = new JSONObject(requestBody); + + String sql = jsonRequest.getString("sql"); + boolean disallowMutations = jsonRequest.getBoolean("disallowMutations"); + + // Validate SQL type against allowMutations flag + if (disallowMutations && isMutationQuery(sql)) { + sendResponse(exchange, 400, "Mutations not allowed"); + return; + } + + // Execute SQL and get results + JSONArray results = executeSQLQuery(sql, connectionProps); + + // Send response + sendResponse(exchange, 200, results.toString()); + + } catch (SQLException e) { + e.printStackTrace(); + sendResponse(exchange, 500, "Database Error: " + e.getMessage()); + } catch (Exception e) { + e.printStackTrace(); + sendResponse(exchange, 500, "Internal Server Error: " + e.getMessage()); + } + } + + private boolean isMutationQuery(String sql) { + String upperSql = sql.trim().toUpperCase(); + return upperSql.startsWith("INSERT") || + upperSql.startsWith("UPDATE") || + upperSql.startsWith("DELETE") || + upperSql.startsWith("DROP") || + upperSql.startsWith("CREATE") || + upperSql.startsWith("ALTER"); + } + + private JSONArray executeSQLQuery(String sql, Properties connectionProps) throws SQLException { + JSONArray jsonArray = new JSONArray(); + + // Create a new connection for each request using the provided properties + try (Connection conn = DriverManager.getConnection(JDBC_URL, connectionProps); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + + while (rs.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = rs.getObject(i); + row.put(columnName, value); + } + jsonArray.put(row); + } + } + + return jsonArray; + } + + private void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "application/json"); + byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(statusCode, responseBytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(responseBytes); + } + } + } +} \ No newline at end of file