@@ -213,13 +213,16 @@ public void install(@NonNull Jooby application) {
213213
214214 ServiceRegistry registry = application .getServices ();
215215 ServiceKey <DataSource > key = ServiceKey .key (DataSource .class , database );
216- /** Global default database: */
216+ /* Global default database: */
217217 registry .putIfAbsent (KEY , dataSource );
218218
219- /** Specific access: */
219+ /* Specific access: */
220220 registry .put (key , dataSource );
221+ /* List access: */
222+ registry .listOf (DataSource .class ).add (dataSource );
223+ registry .listOf (HikariDataSource .class ).add (dataSource );
221224
222- application .onStop (dataSource :: close );
225+ application .onStop (dataSource );
223226 }
224227
225228 /**
@@ -231,13 +234,11 @@ public void install(@NonNull Jooby application) {
231234 * @param url Jdbc connection string (a.k.a jdbc url)
232235 * @return Database type or given jdbc connection string for unknown or bad urls.
233236 */
234- public static @ NonNull String databaseType (@ NonNull String url ) {
235- String type =
236- Arrays .stream (url .toLowerCase ().split (":" ))
237- .filter (token -> !SKIP_TOKENS .contains (token ))
238- .findFirst ()
239- .orElse (url );
240- return type ;
237+ public static String databaseType (@ NonNull String url ) {
238+ return Arrays .stream (url .toLowerCase ().split (":" ))
239+ .filter (token -> !SKIP_TOKENS .contains (token ))
240+ .findFirst ()
241+ .orElse (url );
241242 }
242243
243244 /**
@@ -288,71 +289,151 @@ private static Map<String, Object> defaults(String database, Environment env) {
288289 defaults .put (
289290 "maximumPoolSize" ,
290291 Math .max (MINIMUM_SIZE , Runtime .getRuntime ().availableProcessors () * WORKER_FACTOR ));
291- if ("derby" .equals (database )) {
292- // url => jdbc:derby:${db};create=true
293- defaults .put ("dataSourceClassName" , "org.apache.derby.jdbc.ClientDataSource" );
294- } else if ("db2" .equals (database )) {
295- // url => jdbc:db2://127.0.0.1:50000/SAMPLE
296- defaults .put ("dataSourceClassName" , "com.ibm.db2.jcc.DB2SimpleDataSource" );
297- } else if ("h2" .equals (database )) {
298- // url => mem, fs or jdbc:h2:${db}
299- defaults .put ("dataSourceClassName" , "org.h2.jdbcx.JdbcDataSource" );
300- defaults .put ("dataSource.user" , "sa" );
301- defaults .put ("dataSource.password" , "" );
302- } else if ("hsqldb" .equals (database )) {
303- // url => jdbc:hsqldb:file:${db}
304- defaults .put ("dataSourceClassName" , "org.hsqldb.jdbc.JDBCDataSource" );
305- } else if ("mariadb" .equals (database )) {
306- // url jdbc:mariadb://<host>:<port>/<database>?<key1>=<value1>&<key2>=<value2>...
307- defaults .put ("dataSourceClassName" , "org.mariadb.jdbc.MySQLDataSource" );
308- } else if ("mysql" .equals (database )) {
309- // url jdbc:mysql://<host>:<port>/<database>?<key1>=<value1>&<key2>=<value2>...
310- // 6.x
311- env .loadClass ("com.mysql.cj.jdbc.MysqlDataSource" )
312- .ifPresent (klass -> defaults .put ("dataSourceClassName" , klass .getName ()));
313- // 5.x
314- if (!defaults .containsKey ("dataSourceClassName" )) {
315- env .loadClass ("com.mysql.jdbc.jdbc2.optional.MysqlDataSource" )
316- .ifPresent (
317- klass -> {
318- defaults .put ("dataSourceClassName" , klass .getName ());
319- defaults .put (
320- "dataSource.encoding" , env .getConfig ().getString (AvailableSettings .CHARSET ));
321- defaults .put ("dataSource.cachePrepStmts" , true );
322- defaults .put ("dataSource.prepStmtCacheSize" , MYSQL5_STT_CACHE_SIZE );
323- defaults .put ("dataSource.prepStmtCacheSqlLimit" , MYSQL5_STT_CACHE_SQL_LIMIT );
324- defaults .put ("dataSource.useServerPrepStmts" , true );
325- });
292+ if (database == null ) {
293+ return defaults ;
294+ }
295+ switch (database ) {
296+ case "derby" ->
297+ // url => jdbc:derby:${db};create=true
298+ defaults .put ("dataSourceClassName" , "org.apache.derby.jdbc.ClientDataSource" );
299+ case "db2" ->
300+ // url => jdbc:db2://127.0.0.1:50000/SAMPLE
301+ defaults .put ("dataSourceClassName" , "com.ibm.db2.jcc.DB2SimpleDataSource" );
302+ case "h2" -> {
303+ // url => mem, fs or jdbc:h2:${db}
304+ defaults .put ("dataSourceClassName" , "org.h2.jdbcx.JdbcDataSource" );
305+ defaults .put ("dataSource.user" , "sa" );
306+ defaults .put ("dataSource.password" , "" );
307+ }
308+ case "hsqldb" ->
309+ // url => jdbc:hsqldb:file:${db}
310+ defaults .put ("dataSourceClassName" , "org.hsqldb.jdbc.JDBCDataSource" );
311+ case "mariadb" ->
312+ // url jdbc:mariadb://<host>:<port>/<database>?<key1>=<value1>&<key2>=<value2>...
313+ defaults .put ("dataSourceClassName" , "org.mariadb.jdbc.MySQLDataSource" );
314+ case "mysql" -> {
315+ // url jdbc:mysql://<host>:<port>/<database>?<key1>=<value1>&<key2>=<value2>...
316+ // 6.x
317+ env .loadClass ("com.mysql.cj.jdbc.MysqlDataSource" )
318+ .ifPresent (klass -> defaults .put ("dataSourceClassName" , klass .getName ()));
319+ // 5.x
320+ if (!defaults .containsKey ("dataSourceClassName" )) {
321+ env .loadClass ("com.mysql.jdbc.jdbc2.optional.MysqlDataSource" )
322+ .ifPresent (
323+ klass -> {
324+ defaults .put ("dataSourceClassName" , klass .getName ());
325+ defaults .put (
326+ "dataSource.encoding" ,
327+ env .getConfig ().getString (AvailableSettings .CHARSET ));
328+ defaults .put ("dataSource.cachePrepStmts" , true );
329+ defaults .put ("dataSource.prepStmtCacheSize" , MYSQL5_STT_CACHE_SIZE );
330+ defaults .put ("dataSource.prepStmtCacheSqlLimit" , MYSQL5_STT_CACHE_SQL_LIMIT );
331+ defaults .put ("dataSource.useServerPrepStmts" , true );
332+ });
333+ }
326334 }
327- } else if ("sqlserver" .equals (database )) {
328- // url =>
329- // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]]
330- defaults .put ("dataSourceClassName" , "com.microsoft.sqlserver.jdbc.SQLServerDataSource" );
331- } else if ("oracle" .equals (database )) {
332- // url => jdbc:oracle:thin:@//<host>:<port>/<service_name>
333- defaults .put ("dataSourceClassName" , "oracle.jdbc.pool.OracleDataSource" );
334- } else if ("pgsql" .equals (database )) {
335- // url => jdbc:pgsql://<server>[:<port>]/<database>
336- defaults .put ("dataSourceClassName" , "com.impossibl.postgres.jdbc.PGDataSource" );
337- } else if ("postgresql" .equals (database )) {
338- // url => jdbc:postgresql://host:port/database
339- defaults .put ("dataSourceClassName" , "org.postgresql.ds.PGSimpleDataSource" );
340- } else if ("sybase" .equals (database )) {
341- // url => jdbc:jtds:sybase://<host>[:<port>][/<database_name>]
342- defaults .put ("dataSourceClassName" , "com.sybase.jdbcx.SybDataSource" );
343- } else if ("firebirdsql" .equals (database )) {
344- // jdbc:firebirdsql:host[/port]:<database>
345- defaults .put ("dataSourceClassName" , "org.firebirdsql.pool.FBSimpleDataSource" );
346- } else if ("sqlite" .equals (database )) {
347- // jdbc:sqlite:${db}
348- defaults .put ("dataSourceClassName" , "org.sqlite.SQLiteDataSource" );
349- } else if ("log4jdbc" .equals (database )) {
350- // jdbc:log4jdbc:${dbtype}:${db}
351- defaults .put ("driverClassName" , "net.sf.log4jdbc.DriverSpy" );
335+ case "sqlserver" ->
336+ // url =>
337+ // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]]
338+ defaults .put ("dataSourceClassName" , "com.microsoft.sqlserver.jdbc.SQLServerDataSource" );
339+ case "oracle" ->
340+ // url => jdbc:oracle:thin:@//<host>:<port>/<service_name>
341+ defaults .put ("dataSourceClassName" , "oracle.jdbc.pool.OracleDataSource" );
342+ case "pgsql" ->
343+ // url => jdbc:pgsql://<server>[:<port>]/<database>
344+ defaults .put ("dataSourceClassName" , "com.impossibl.postgres.jdbc.PGDataSource" );
345+ case "postgresql" , "cockroach" , "yugabyte" ->
346+ // url => jdbc:postgresql://host:port/database
347+ defaults .put ("dataSourceClassName" , "org.postgresql.ds.PGSimpleDataSource" );
348+ case "sybase" ->
349+ // url => jdbc:jtds:sybase://<host>[:<port>][/<database_name>]
350+ defaults .put ("dataSourceClassName" , "com.sybase.jdbcx.SybDataSource" );
351+ case "firebirdsql" ->
352+ // jdbc:firebirdsql:host[/port]:<database>
353+ defaults .put ("dataSourceClassName" , "org.firebirdsql.pool.FBSimpleDataSource" );
354+ case "sqlite" ->
355+ // jdbc:sqlite:${db}
356+ defaults .put ("dataSourceClassName" , "org.sqlite.SQLiteDataSource" );
357+ // --- OLAP & Analytics ---
358+ case "clickhouse" ->
359+ // jdbc:clickhouse://<host>:<port>/<database>
360+ defaults .put ("dataSourceClassName" , "com.clickhouse.jdbc.ClickHouseDataSource" );
361+ case "snowflake" ->
362+ // jdbc:snowflake://<account>.snowflakecomputing.com/?<options>
363+ defaults .put ("driverClassName" , "net.snowflake.client.jdbc.SnowflakeDriver" );
364+ case "redshift" ->
365+ // jdbc:redshift://<cluster>.<region>.redshift.amazonaws.com:<port>/<database>
366+ defaults .put ("driverClassName" , "com.amazon.redshift.Driver" );
367+ case "trino" ->
368+ // jdbc:trino://<host>:<port>/<catalog>/<schema>
369+ defaults .put ("driverClassName" , "io.trino.jdbc.TrinoDriver" );
370+ // --- Proxies & Wrappers ---
371+ case "log4jdbc" ->
372+ // jdbc:log4jdbc:${dbtype}:${db}
373+ defaults .put ("driverClassName" , "net.sf.log4jdbc.DriverSpy" );
374+ case "otel" ->
375+ // jdbc:otel:${dbtype}:${db}
376+ defaults .put (
377+ "driverClassName" , "io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver" );
352378 }
353379 return defaults ;
354380 }
355381
382+ /**
383+ * Forces the JVM to load and execute the static initialization block of the underlying JDBC
384+ * Driver. This is specifically required for wrappers like OpenTelemetry that rely on
385+ * java.sql.DriverManager instead of direct DataSource instantiation.
386+ *
387+ * @param database The target database type (e.g., "mysql", "postgresql")
388+ * @param env The Jooby environment providing the classloader
389+ */
390+ private static void forceLoadDriver (String database , Environment env ) {
391+ if (database == null ) {
392+ return ;
393+ }
394+
395+ // Map the database string to its explicit java.sql.Driver implementation
396+ var driverClassName =
397+ switch (database ) {
398+ case "derby" -> "org.apache.derby.jdbc.ClientDriver" ;
399+ case "db2" -> "com.ibm.db2.jcc.DB2Driver" ;
400+ case "h2" -> "org.h2.Driver" ;
401+ case "hsqldb" -> "org.hsqldb.jdbc.JDBCDriver" ;
402+ case "mariadb" -> "org.mariadb.jdbc.Driver" ;
403+ case "mysql" -> "com.mysql.cj.jdbc.Driver" ; // Modern 6.x/8.x Driver
404+ case "sqlserver" -> "com.microsoft.sqlserver.jdbc.SQLServerDriver" ;
405+ case "oracle" -> "oracle.jdbc.OracleDriver" ;
406+ case "pgsql" -> "com.impossibl.postgres.jdbc.PGDriver" ;
407+ case "postgresql" , "cockroach" , "yugabyte" -> "org.postgresql.Driver" ;
408+ case "sybase" -> "com.sybase.jdbc4.jdbc.SybDriver" ;
409+ case "firebirdsql" -> "org.firebirdsql.jdbc.FBDriver" ;
410+ case "sqlite" -> "org.sqlite.JDBC" ;
411+ // --- OLAP & Analytics ---
412+ case "clickhouse" -> "com.clickhouse.jdbc.ClickHouseDriver" ;
413+ case "snowflake" -> "net.snowflake.client.jdbc.SnowflakeDriver" ;
414+ case "redshift" -> "com.amazon.redshift.Driver" ;
415+ case "trino" -> "io.trino.jdbc.TrinoDriver" ;
416+ default -> null ;
417+ };
418+
419+ if (driverClassName != null ) {
420+ try {
421+ // The 'true' flag is the magic key: it forces the static {} block to execute,
422+ // registering the driver globally with Java's DriverManager.
423+ Class .forName (driverClassName , true , env .getClassLoader ());
424+ } catch (ClassNotFoundException e ) {
425+ // Graceful fallback for legacy MySQL 5.x users if the modern driver is missing
426+ if ("mysql" .equals (database )) {
427+ try {
428+ Class .forName ("com.mysql.jdbc.Driver" , true , env .getClassLoader ());
429+ } catch (ClassNotFoundException ignore ) {
430+ // Ignore missing driver; let the standard JDBC connection handle the failure later
431+ }
432+ }
433+ }
434+ }
435+ }
436+
356437 static HikariConfig build (Environment env , String database ) {
357438 Properties properties ;
358439 Config config = env .getConfig ();
@@ -379,7 +460,7 @@ static HikariConfig build(Environment env, String database) {
379460 dumpProperties (config , dbname , "dataSource." , properties ::setProperty );
380461 }
381462
382- /** *.dataSource AND *.hikari */
463+ /* *.dataSource AND *.hikari */
383464 Stream .of (dbkey , dbname )
384465 .filter (Objects ::nonNull )
385466 .distinct ()
@@ -403,7 +484,10 @@ static HikariConfig build(Environment env, String database) {
403484 configuration .remove ("dataSource.url" );
404485 configuration .setProperty ("jdbcUrl" , dburl );
405486 }
406-
487+ // wake driver for otel
488+ if (dburl != null && dburl .startsWith ("jdbc:otel:" )) {
489+ forceLoadDriver (databaseType (dburl .replace (":otel:" , ":" )), env );
490+ }
407491 if (dbtype == null ) {
408492 String poolName =
409493 Stream .of (
0 commit comments