diff --git a/src/main/java/org/ohdsi/webapi/arachne/commons/types/DBMSType.java b/src/main/java/org/ohdsi/webapi/arachne/commons/types/DBMSType.java index afd2e2e9ca..01c2f2665f 100644 --- a/src/main/java/org/ohdsi/webapi/arachne/commons/types/DBMSType.java +++ b/src/main/java/org/ohdsi/webapi/arachne/commons/types/DBMSType.java @@ -35,7 +35,8 @@ public enum DBMSType { HIVE("Apache Hive", "hive"), SPARK("Spark", "spark"), SNOWFLAKE("Snowflake", "snowflake"), - SYNAPSE("Azure Synapse", "synapse"); + SYNAPSE("Azure Synapse", "synapse"), + TREXSQL("TrexSQL", "trexsql"); private String label; // For further pass into SqlRender.translateSql as "targetDialect" and DatabaseConnector as "dbms" diff --git a/src/main/java/org/ohdsi/webapi/arachne/datasource/util/ConnectionParamsParser.java b/src/main/java/org/ohdsi/webapi/arachne/datasource/util/ConnectionParamsParser.java index 5595090085..3cfa744873 100644 --- a/src/main/java/org/ohdsi/webapi/arachne/datasource/util/ConnectionParamsParser.java +++ b/src/main/java/org/ohdsi/webapi/arachne/datasource/util/ConnectionParamsParser.java @@ -29,6 +29,7 @@ private static ConnectionStringParser getParser(DBMSType dbmsType) { case PDW: case SNOWFLAKE: case SYNAPSE: + case TREXSQL: return new GenericParser(); case REDSHIFT: return new RedshiftParser(); diff --git a/src/main/java/org/ohdsi/webapi/common/DBMSType.java b/src/main/java/org/ohdsi/webapi/common/DBMSType.java index d84ad162dd..3f74bf2abc 100644 --- a/src/main/java/org/ohdsi/webapi/common/DBMSType.java +++ b/src/main/java/org/ohdsi/webapi/common/DBMSType.java @@ -35,7 +35,8 @@ public enum DBMSType { HIVE("Apache Hive", "hive"), SPARK("Spark", "spark"), SNOWFLAKE("Snowflake", "snowflake"), - SYNAPSE("Azure Synapse", "synapse"); + SYNAPSE("Azure Synapse", "synapse"), + TREXSQL("TrexSQL", "trexsql"); private String label; // For further pass into SqlRender.translateSql as "targetDialect" and DatabaseConnector as "dbms" diff --git a/src/main/java/org/ohdsi/webapi/source/TrexsqlDialect.java b/src/main/java/org/ohdsi/webapi/source/TrexsqlDialect.java new file mode 100644 index 0000000000..a11209190d --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/source/TrexsqlDialect.java @@ -0,0 +1,86 @@ +package org.ohdsi.webapi.source; + +import org.ohdsi.sql.SqlTranslate; +import org.ohdsi.webapi.common.DBMSType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Registers "trexsql" as a SqlRender target dialect. + * + * SqlRender has no trexsql target of its own, so its patterns for the engine + * trexsql is built on are copied under the trexsql name. SqlTranslate keeps a + * single JVM-wide pattern table, so this also covers libraries that translate + * internally (cohort and characterization SQL). + */ +@Component +public class TrexsqlDialect { + + public static final String NAME = DBMSType.TREXSQL.getOhdsiDB(); + + // The SqlRender target whose patterns trexsql reuses. + static final String BASE_DIALECT = "duckdb"; + + private static final String PATTERNS_RESOURCE = "/inst/csv/replacementPatterns.csv"; + private static final Logger log = LoggerFactory.getLogger(TrexsqlDialect.class); + + private static volatile boolean registered; + + @PostConstruct + void init() { + register(); + } + + public static synchronized void register() { + if (registered) { + return; + } + List lines = readBundledPatterns(); + List merged = new ArrayList<>(lines); + String prefix = BASE_DIALECT + ","; + for (String line : lines) { + if (line.startsWith(prefix)) { + merged.add(NAME + line.substring(BASE_DIALECT.length())); + } + } + if (merged.size() == lines.size()) { + throw new IllegalStateException("SqlRender has no " + BASE_DIALECT + " patterns to register " + NAME + " from"); + } + try { + Path file = Files.createTempFile("sqlrender-patterns-", ".csv"); + file.toFile().deleteOnExit(); + Files.write(file, merged, StandardCharsets.UTF_8); + SqlTranslate.setReplacementPatterns(file.toString()); + } catch (IOException e) { + throw new UncheckedIOException("Could not register the " + NAME + " SqlRender dialect", e); + } + registered = true; + log.info("Registered SqlRender dialect {} ({} patterns)", NAME, merged.size() - lines.size()); + } + + private static List readBundledPatterns() { + try (InputStream in = SqlTranslate.class.getResourceAsStream(PATTERNS_RESOURCE)) { + if (in == null) { + throw new IllegalStateException("SqlRender resource " + PATTERNS_RESOURCE + " not found"); + } + return new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + .lines().collect(Collectors.toList()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/test/java/org/ohdsi/webapi/source/TrexsqlDialectTest.java b/src/test/java/org/ohdsi/webapi/source/TrexsqlDialectTest.java new file mode 100644 index 0000000000..c1bd7d39f0 --- /dev/null +++ b/src/test/java/org/ohdsi/webapi/source/TrexsqlDialectTest.java @@ -0,0 +1,42 @@ +package org.ohdsi.webapi.source; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.ohdsi.sql.SqlTranslate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class TrexsqlDialectTest { + + private static final String SQL = + "SELECT TOP 10 person_id, DATEADD(day, 30, observation_period_start_date) AS d " + + "INTO #codesets FROM @cdm.observation_period WHERE ISNULL(period_type_concept_id, 0) > 0;"; + + @BeforeClass + public static void registerDialect() { + TrexsqlDialect.register(); + } + + @Test + public void translatesLikeItsBaseDialect() { + String trexsql = SqlTranslate.translateSql(SQL, TrexsqlDialect.NAME); + assertEquals(SqlTranslate.translateSql(SQL, TrexsqlDialect.BASE_DIALECT), trexsql); + assertNotEquals(SQL, trexsql); + } + + @Test + public void registerIsIdempotent() { + TrexsqlDialect.register(); + assertEquals( + SqlTranslate.translateSql(SQL, TrexsqlDialect.BASE_DIALECT), + SqlTranslate.translateSql(SQL, TrexsqlDialect.NAME)); + } + + @Test + public void otherDialectsAreUnchanged() { + assertNotEquals( + SqlTranslate.translateSql(SQL, "postgresql"), + SqlTranslate.translateSql(SQL, TrexsqlDialect.NAME)); + } +} diff --git a/src/test/java/org/ohdsi/webapi/util/DataSourceDTOParserTest.java b/src/test/java/org/ohdsi/webapi/util/DataSourceDTOParserTest.java index f4976965f2..799702fc86 100644 --- a/src/test/java/org/ohdsi/webapi/util/DataSourceDTOParserTest.java +++ b/src/test/java/org/ohdsi/webapi/util/DataSourceDTOParserTest.java @@ -30,6 +30,7 @@ public class DataSourceDTOParserTest { public static final String ORACLE_WITH_PWD_CONN_STR = "jdbc:oracle:thin:scott/tiger@myhost:1521:orcl"; public static final String HIVE_CONN_STR = "jdbc:hive2://sandbox-hdp.hortonworks.com:2181/synpuf_531_orc;serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=hiveserver2"; public static final String IRIS_CONN_STR = "jdbc:IRIS://localhost:1972/USER"; + public static final String TREXSQL_CONN_STR = "jdbc:trexsql://trex.example.com:9494"; @Test public void parseDTO() { @@ -112,6 +113,12 @@ public void parseDTO() { assertThat(dto.getUsername(), is(nullValue())); assertThat(dto.getPassword(), is(nullValue())); + dto = DataSourceDTOParser.parseDTO(getTrexsqlSource()); + assertThat(dto.getType(), is(DBMSType.TREXSQL)); + assertThat(dto.getConnectionString(), is(TREXSQL_CONN_STR)); + assertThat(dto.getUsername(), is("webapi")); + assertThat(dto.getPassword(), is("access-token")); + // IRIS is not supported in DBMSType enum, commenting out // dto = DataSourceDTOParser.parseDTO(getIRISSource()); // assertThat(dto.getType(), is(DBMSType.IRIS)); @@ -219,4 +226,13 @@ private Source getIRISSource() { source.setSourceConnection(IRIS_CONN_STR); return source; } -} \ No newline at end of file + + private Source getTrexsqlSource() { + Source source = new Source(); + source.setSourceDialect("trexsql"); + source.setSourceConnection(TREXSQL_CONN_STR); + source.setUsername("webapi"); + source.setPassword("access-token"); + return source; + } +}