From 9eabeef052f6b6a31ecc1998c5de59e31f71acea Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:59:21 +0800 Subject: [PATCH] Add trexsql as a source dialect Sources can now use dialect trexsql with a jdbc:trexsql:// connection string; the trexsql plugin provides the driver. SqlRender has no trexsql target, so TrexsqlDialect registers one at startup that reuses the patterns of the engine trexsql is built on. SqlTranslate keeps one pattern table per JVM, so cohort and characterization SQL that is translated inside libraries picks it up too. --- .../arachne/commons/types/DBMSType.java | 3 +- .../util/ConnectionParamsParser.java | 1 + .../org/ohdsi/webapi/common/DBMSType.java | 3 +- .../ohdsi/webapi/source/TrexsqlDialect.java | 86 +++++++++++++++++++ .../webapi/source/TrexsqlDialectTest.java | 42 +++++++++ .../webapi/util/DataSourceDTOParserTest.java | 18 +++- 6 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/ohdsi/webapi/source/TrexsqlDialect.java create mode 100644 src/test/java/org/ohdsi/webapi/source/TrexsqlDialectTest.java 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; + } +}