diff --git a/README.md b/README.md
index cee30eed7d..84e6e2fd70 100644
--- a/README.md
+++ b/README.md
@@ -110,4 +110,4 @@ At this point you have the application running and admin account operational. To
and at least one CDM database. They are covered in the respective documentation sections.
## License
-OHDSI WebAPI is licensed under Apache License 2.0
+OHDSI WebAPI is licensed under Apache License 2.0
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 19882e0085..3bf51d8690 100644
--- a/pom.xml
+++ b/pom.xml
@@ -305,6 +305,11 @@
false
+
+
+ false
+ ./data/cache
+
@@ -420,6 +425,7 @@
false
+ false
org.ohdsi.webapi.WebApi
--add-opens java.base/java.lang=ALL-UNNAMED
@@ -460,6 +466,9 @@
--add-opens java.naming/com.sun.jndi.ldap=ALL-UNNAMED
--add-exports java.naming/com.sun.jndi.ldap=ALL-UNNAMED
+
+ org.springframework.boot.logging.log4j2.Log4J2LoggingSystem
+
@@ -482,6 +491,9 @@
3.5.2
${skipITtests}
+
+ org.springframework.boot.logging.log4j2.Log4J2LoggingSystem
+
@@ -559,6 +571,10 @@
repo.ohdsi.org
https://repo.ohdsi.org/nexus/content/groups/public
+
+ jitpack.io
+ https://jitpack.io
+
@@ -1240,9 +1256,31 @@
com.fasterxml.jackson.datatype
jackson-datatype-jsr310
+
+ com.github.p-hoffmann
+ trexsql-ext
+ v0.1.2
+
+
+ tcache
+
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ true
+
+
+
+
+
webapi-oracle
diff --git a/src/main/java/org/ohdsi/webapi/WebApi.java b/src/main/java/org/ohdsi/webapi/WebApi.java
index 2fed22d3fc..e4bbf74fcb 100644
--- a/src/main/java/org/ohdsi/webapi/WebApi.java
+++ b/src/main/java/org/ohdsi/webapi/WebApi.java
@@ -17,7 +17,6 @@
/**
* OHDSI WebAPI Spring Boot Application
*
- * Supports both JAR and WAR deployment:
* - JAR: java -jar WebAPI.jar (embedded Tomcat)
* - WAR: Deploy to external servlet container (mvn package -Pwar)
*/
diff --git a/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLConfig.java b/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLConfig.java
new file mode 100644
index 0000000000..40d3dd7dfb
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLConfig.java
@@ -0,0 +1,65 @@
+package org.ohdsi.webapi.trexsql;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Configuration properties for trexsql integration.
+ * Maps to trexsql.* in application properties.
+ */
+@Configuration
+@ConfigurationProperties(prefix = "trexsql")
+public class TrexSQLConfig {
+
+ private boolean enabled = false;
+ private String cachePath = "./data/cache";
+ private String extensionsPath;
+ private Map sources = new HashMap<>();
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getCachePath() {
+ return cachePath;
+ }
+
+ public void setCachePath(String cachePath) {
+ this.cachePath = cachePath;
+ }
+
+ public String getExtensionsPath() {
+ return extensionsPath;
+ }
+
+ public void setExtensionsPath(String extensionsPath) {
+ this.extensionsPath = extensionsPath;
+ }
+
+ public Map getSources() {
+ return sources;
+ }
+
+ public void setSources(Map sources) {
+ this.sources = sources;
+ }
+
+ public TrexSQLSourceConfig getSourceConfig(String sourceKey) {
+ return sources.get(sourceKey);
+ }
+
+ public boolean isEnabledForSource(String sourceKey) {
+ if (!enabled) {
+ return false;
+ }
+ TrexSQLSourceConfig sourceConfig = sources.get(sourceKey);
+ return sourceConfig != null && sourceConfig.isEnabled();
+ }
+}
diff --git a/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLInstanceManager.java b/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLInstanceManager.java
new file mode 100644
index 0000000000..36a6997018
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLInstanceManager.java
@@ -0,0 +1,105 @@
+package org.ohdsi.webapi.trexsql;
+
+import org.trex.Trexsql;
+import jakarta.annotation.PreDestroy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Singleton manager for the TrexSQL instance.
+ * Provides lazy initialization and graceful shutdown.
+ */
+@Component
+@ConditionalOnProperty(name = "trexsql.enabled", havingValue = "true", matchIfMissing = false)
+public class TrexSQLInstanceManager {
+
+ private static final Logger log = LoggerFactory.getLogger(TrexSQLInstanceManager.class);
+
+ private final TrexSQLConfig config;
+ private volatile Object trexsqlDb = null;
+ private final ReentrantLock initLock = new ReentrantLock();
+
+ public TrexSQLInstanceManager(TrexSQLConfig config) {
+ this.config = config;
+ }
+
+ public Object getInstance() {
+ if (!config.isEnabled()) {
+ throw new IllegalStateException("TrexSQL is not enabled");
+ }
+
+ if (trexsqlDb == null) {
+ initLock.lock();
+ try {
+ if (trexsqlDb == null) {
+ log.info("Initializing TrexSQL instance");
+ trexsqlDb = Trexsql.init(buildConfig());
+ log.info("TrexSQL instance initialized successfully");
+ }
+ } finally {
+ initLock.unlock();
+ }
+ }
+ return trexsqlDb;
+ }
+
+ public boolean isAvailable() {
+ if (!config.isEnabled() || trexsqlDb == null) {
+ return false;
+ }
+ try {
+ return Trexsql.isRunning(trexsqlDb);
+ } catch (Exception e) {
+ log.warn("Error checking TrexSQL status: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ public boolean isAttached(String databaseCode) {
+ if (trexsqlDb == null) {
+ return false;
+ }
+ try {
+ return Trexsql.isAttached(trexsqlDb, databaseCode);
+ } catch (Exception e) {
+ log.warn("Error checking if database {} is attached: {}", databaseCode, e.getMessage());
+ return false;
+ }
+ }
+
+ private Map buildConfig() {
+ Map initConfig = new HashMap<>();
+
+ if (config.getExtensionsPath() != null && !config.getExtensionsPath().isEmpty()) {
+ initConfig.put("extensions-path", config.getExtensionsPath());
+ }
+
+ return initConfig;
+ }
+
+ @PreDestroy
+ public void shutdown() {
+ initLock.lock();
+ try {
+ if (trexsqlDb != null) {
+ log.info("Shutting down TrexSQL instance");
+ try {
+ Trexsql.shutdown(trexsqlDb);
+ log.info("TrexSQL instance shut down successfully");
+ } catch (Exception e) {
+ log.error("Error shutting down TrexSQL instance: {}", e.getMessage(), e);
+ } finally {
+ trexsqlDb = null;
+ }
+ }
+ } finally {
+ initLock.unlock();
+ }
+ }
+}
diff --git a/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLSearchProvider.java b/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLSearchProvider.java
new file mode 100644
index 0000000000..842d8b9100
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLSearchProvider.java
@@ -0,0 +1,122 @@
+package org.ohdsi.webapi.trexsql;
+
+import org.ohdsi.vocabulary.Concept;
+import org.ohdsi.vocabulary.SearchProvider;
+import org.ohdsi.vocabulary.SearchProviderConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SearchProvider implementation using TrexSQL.
+ */
+@Component
+@ConditionalOnProperty(name = "trexsql.enabled", havingValue = "true", matchIfMissing = false)
+public class TrexSQLSearchProvider implements SearchProvider {
+
+ private static final Logger log = LoggerFactory.getLogger(TrexSQLSearchProvider.class);
+
+ private static final int TREXSQL_PRIORITY = 1;
+
+ private final TrexSQLService trexsqlService;
+ private final TrexSQLConfig config;
+
+ public TrexSQLSearchProvider(TrexSQLService trexsqlService, TrexSQLConfig config) {
+ this.trexsqlService = trexsqlService;
+ this.config = config;
+ }
+
+ @Override
+ public boolean supports(String vocabularyVersionKey) {
+ return config.isEnabled()
+ && trexsqlService.isEnabledForSource(vocabularyVersionKey)
+ && trexsqlService.isCacheAvailable(vocabularyVersionKey);
+ }
+
+ @Override
+ public int getPriority() {
+ return TREXSQL_PRIORITY;
+ }
+
+ @Override
+ public Collection executeSearch(SearchProviderConfig searchConfig, String query, String rows) throws Exception {
+ String sourceKey = searchConfig.getSourceKey();
+
+ if (!trexsqlService.isEnabledForSource(sourceKey)) {
+ log.debug("TrexSQL not enabled for source {}", sourceKey);
+ throw new IllegalStateException("TrexSQL not enabled for source: " + sourceKey);
+ }
+
+ if (!trexsqlService.isCacheAvailable(sourceKey)) {
+ log.debug("Cache not available for source {}", sourceKey);
+ throw new IllegalStateException("TrexSQL cache not available for source: " + sourceKey);
+ }
+
+ int maxRows = parseRows(rows);
+ log.debug("TrexSQL search for source {} with query: {}", sourceKey, query);
+
+ try {
+ List