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