Skip to content

Commit

Permalink
feat: Add possibility to specify translator factories directly. (#568)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-simons authored Mar 13, 2024
1 parent 1df1165 commit 7a09ab7
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 3 deletions.
7 changes: 6 additions & 1 deletion docs/src/main/asciidoc/modules/ROOT/pages/sql2cypher.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ The former is provided for two reasons: It allows us to distribute the driver wi

Translators can be chained, and you can have as many translators on the classpath as you want.
They will be ordered by a configurable precedence with our default implementation having the lowest precedence.
Thus you can have for example a custom translator that takes care of a fixed set of queries, and if it cannot translate another, it will just be passed down to our implementation.
Thus, you can have for example a custom translator that takes care of a fixed set of queries, and if it cannot translate another, it will just be passed down to our implementation.

Translating arbitrary SQL queries to Cypher is an opinionated task as there is no right way to map table names to objects in the graph: A table name can be used as is as a label, you might want to transform it to a singular form etc. And then we haven't even started how to map relationships: Do you want to have relationship types derived from a join table, a join column (in that case, which one?) or the name of a foreign key?

We made some assumptions that we find to match various use cases and instead of providing configuration and more code to cater for all scenarios, we offer the possibility to write your own translation layer.
The driver will use the standard Java service loader mechanism to find an implementation of the SPI on the module- or classpath.

NOTE: Some tools like Tableau use a class-loader that won't let the driver use the standard Java service loader mechanism.
For these scenarios we provide an additional configuration property named `translatorFactory`.
Set this to `DEFAULT` for directly loading our default implementation or to a fully-qualified classname for any other factory.
Be aware that either our default implementation or your custom one needs to be on the classpath nevertheless.

== Translating SQL to Cypher

There's only one requirement to enable SQL to Cypher translation:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
*/
package org.neo4j.jdbc.it.cp;

import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

import org.jooq.impl.ParserException;
import org.junit.jupiter.api.Test;
Expand All @@ -29,6 +31,42 @@

class TranslationIT extends IntegrationTestBase {

@Test
void shouldLoadTranslatorDirectly() throws SQLException {

var url = "jdbc:neo4j://%s:%d".formatted(this.neo4j.getHost(), this.neo4j.getMappedPort(7687));
var driver = DriverManager.getDriver(url);
var properties = new Properties();
properties.put("user", "neo4j");
properties.put("password", this.neo4j.getAdminPassword());
properties.put("translatorFactory", "default");
properties.put("enableSQLTranslation", "true");

try (var con = driver.connect(url, properties);
var stmt = con.createStatement();
var rs = stmt.executeQuery("SELECT 1")) {
assertThat(rs.next()).isTrue();
assertThat(rs.getInt(1)).isOne();
}

}

@Test
void shouldFailWhenLoadingInvalidClassDirectly() throws SQLException {

var url = "jdbc:neo4j://%s:%d".formatted(this.neo4j.getHost(), this.neo4j.getMappedPort(7687));
var driver = DriverManager.getDriver(url);
var properties = new Properties();
properties.put("user", "neo4j");
properties.put("password", this.neo4j.getAdminPassword());
properties.put("translatorFactory", "asd");
properties.put("enableSQLTranslation", "true");

assertThatExceptionOfType(SQLException.class).isThrownBy(() -> driver.connect(url, properties))
.withMessage("No translators available");

}

@Test
void shouldTranslateAsterisk() throws SQLException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ private static ResultSet makeColumns(String firstName, String... names) throws S
return personColumns;
}

@Test
void selectNShouldWork() {
assertThat(NON_PRETTY_PRINTING_TRANSLATOR.translate("SELECT 1")).isEqualTo("RETURN 1");
}

@Test
void parsingExceptionMustBeWrapped() {
assertThatIllegalArgumentException().isThrownBy(() -> NON_PRETTY_PRINTING_TRANSLATOR.translate("whatever"))
Expand Down
36 changes: 34 additions & 2 deletions neo4j-jdbc/src/main/java/org/neo4j/jdbc/Neo4jDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.neo4j.jdbc;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.sql.Connection;
Expand All @@ -37,6 +38,7 @@
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -126,6 +128,14 @@ public final class Neo4jDriver implements Neo4jDriverExtensions {
*/
public static final String PROPERTY_SQL_TRANSLATION_CACHING_ENABLED = "cacheSQLTranslations";

/**
* This is an alternative to the automatic configuration of translator factories and
* can be applied to load a single translator. This is helpful in scenarios in which
* an isolated class-loader is used, that prohibits access to the ServiceLoader
* machinery.
*/
public static final String PROPERTY_TRANSLATOR_FACTORY = "translatorFactory";

private static final String PROPERTY_S2C_ALWAYS_ESCAPE_NAMES = "s2c.alwaysEscapeNames";

private static final String PROPERTY_S2C_PRETTY_PRINT_CYPHER = "s2c.prettyPrint";
Expand Down Expand Up @@ -278,10 +288,14 @@ public Connection connect(String url, Properties info) throws SQLException {
var enableTranslationCaching = driverConfig.enableTranslationCaching;
var rewriteBatchedStatements = driverConfig.rewriteBatchedStatements;
var rewritePlaceholders = driverConfig.rewritePlaceholders;
var translatorFactory = driverConfig.misc.get(PROPERTY_TRANSLATOR_FACTORY);

Supplier<List<TranslatorFactory>> translatorFactoriesSupplier = this::getSqlTranslatorFactories;
if (translatorFactory != null && !translatorFactory.isBlank()) {
translatorFactoriesSupplier = () -> getSqlTranslatorFactory(translatorFactory);
}
return new ConnectionImpl(boltConnection,
getSqlTranslatorSupplier(enableSqlTranslation, driverConfig.rawConfig(),
this::getSqlTranslatorFactories),
getSqlTranslatorSupplier(enableSqlTranslation, driverConfig.rawConfig(), translatorFactoriesSupplier),
enableSqlTranslation, enableTranslationCaching, rewriteBatchedStatements, rewritePlaceholders);
}

Expand Down Expand Up @@ -635,6 +649,24 @@ else if (ssl != null && sslMode == null && ssl) {
return new SSLProperties(sslMode, ssl);
}

private List<TranslatorFactory> getSqlTranslatorFactory(String translatorFactory) {

var fqn = "DEFAULT".equalsIgnoreCase(translatorFactory)
? "org.neo4j.jdbc.translator.impl.SqlToCypherTranslatorFactory" : translatorFactory;
try {
@SuppressWarnings("unchecked")
Class<TranslatorFactory> cls = (Class<TranslatorFactory>) Class.forName(fqn);
return List.of(cls.getDeclaredConstructor().newInstance());
}
catch (ClassNotFoundException ex) {
getParentLogger().log(Level.WARNING, "Translator factory {0} not found", new Object[] { fqn });
}
catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
getParentLogger().log(Level.WARNING, ex, () -> "Could not load translator factory");
}
return List.of();
}

/**
* Tries to load all available {@link TranslatorFactory SqlTranslator factories} via
* the {@link ServiceLoader} machinery. Throws an exception if there is no
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,7 @@
<pmd.skip>true</pmd.skip>
<skipTests>true</skipTests>
<sort.skip>true</sort.skip>
<spring-javaformat.skip>true</spring-javaformat.skip>
</properties>
</profile>
</profiles>
Expand Down

0 comments on commit 7a09ab7

Please sign in to comment.