11package code .api .util
22
3- import cats .effect .{ IO , Resource }
3+ import cats .effect .IO
44import cats .effect .unsafe .implicits .global
5- import com . zaxxer . hikari . HikariConfig
5+ import code . util . Helper . MdcLoggable
66import doobie ._
7- import doobie .hikari .HikariTransactor
87import doobie .implicits ._
8+ import doobie .util .transactor .Strategy
9+ import net .liftweb .common .Full
10+ import net .liftweb .db .DB
911
1012import scala .concurrent .{ExecutionContext , Future }
1113
@@ -16,16 +18,22 @@ import scala.concurrent.{ExecutionContext, Future}
1618 * This handles all JDBC types correctly, including SQL Server's NVARCHAR (type -9)
1719 * which Lift's DB.runQuery doesn't handle.
1820 *
19- * IMPORTANT: This uses a SEPARATE HikariCP connection pool from Lift's pool.
20- * This ensures complete isolation and safety - Doobie manages its own connections
21- * without any interference with Lift's connection management.
21+ * TRANSACTION UNIFICATION:
22+ * When called within a Lift HTTP request context, Doobie uses the SAME Connection
23+ * that Lift is holding for the current request transaction (via Transactor.fromConnection).
24+ * This means Doobie queries participate in Lift's transaction boundary:
25+ * - Same connection, same transaction, same commit/rollback
26+ * - Doobie can see uncommitted Lift writes (same session)
27+ * - If Lift rolls back, Doobie's operations are also rolled back
28+ *
29+ * When called outside a Lift request context (e.g., background tasks, schedulers),
30+ * falls back to Lift's shared HikariCP connection pool via Transactor.fromDataSource.
2231 *
2332 * Benefits over DBUtil.runQuery:
2433 * - Type-safe query results via case classes
2534 * - Type-safe parameters (no SQL injection risk)
2635 * - Proper JDBC type handling for all databases
2736 * - Composable queries using cats-effect IO
28- * - Complete isolation from Lift's connection pool
2937 *
3038 * Usage:
3139 * {{{
@@ -45,96 +53,99 @@ import scala.concurrent.{ExecutionContext, Future}
4553 * val result: List[TopApi] = DoobieUtil.runQuery(query)
4654 * }}}
4755 */
48- object DoobieUtil {
56+ object DoobieUtil extends MdcLoggable {
4957
5058 /**
51- * Lazy-initialized HikariCP transactor for Doobie.
52- *
53- * This creates a separate connection pool from Lift's pool, ensuring
54- * complete isolation and safety. The pool is initialized on first use
55- * and kept alive for the lifetime of the application.
59+ * Fallback transactor that shares Lift's HikariCP connection pool.
60+ * Used when no Lift request context is available (background tasks, schedulers).
61+ * Strategy.void: Doobie will not call setAutoCommit/commit/rollback.
5662 */
57- private lazy val transactor : Transactor [IO ] = {
58- val (dbUrl, dbUser, dbPassword) = DBUtil .getDbConnectionParameters
59-
60- val hikariConfig = new HikariConfig ()
61- hikariConfig.setJdbcUrl(dbUrl)
62- hikariConfig.setUsername(dbUser)
63- hikariConfig.setPassword(dbPassword)
64-
65- // Pool configuration - conservative settings for safety
66- hikariConfig.setPoolName(" doobie-pool" )
67- hikariConfig.setMaximumPoolSize(
68- APIUtil .getPropsAsIntValue(" doobie.hikari.maximumPoolSize" , 10 )
69- )
70- hikariConfig.setMinimumIdle(
71- APIUtil .getPropsAsIntValue(" doobie.hikari.minimumIdle" , 2 )
72- )
73- hikariConfig.setConnectionTimeout(
74- APIUtil .getPropsAsLongValue(" doobie.hikari.connectionTimeout" , 30000L )
75- )
76- hikariConfig.setIdleTimeout(
77- APIUtil .getPropsAsLongValue(" doobie.hikari.idleTimeout" , 600000L )
78- )
79- hikariConfig.setMaxLifetime(
80- APIUtil .getPropsAsLongValue(" doobie.hikari.maxLifetime" , 1800000L )
63+ private lazy val fallbackTransactor : Transactor [IO ] = {
64+ val liftDataSource = APIUtil .vendor.HikariDatasource .ds
65+ logger.info(" DoobieUtil: Initialized fallback transactor sharing Lift's HikariCP pool" )
66+ val xa = Transactor .fromDataSource[IO ].apply(
67+ liftDataSource,
68+ ExecutionContext .global
8169 )
70+ xa.copy(strategy0 = Strategy .void)
71+ }
8272
83- // Set driver class based on database type
84- if (dbUrl.contains(" sqlserver" )) {
85- hikariConfig.setDriverClassName(" com.microsoft.sqlserver.jdbc.SQLServerDriver" )
86- } else if (dbUrl.contains(" postgresql" )) {
87- hikariConfig.setDriverClassName(" org.postgresql.Driver" )
88- } else if (dbUrl.contains(" mysql" )) {
89- hikariConfig.setDriverClassName(" com.mysql.cj.jdbc.Driver" )
90- } else if (dbUrl.contains(" h2" )) {
91- hikariConfig.setDriverClassName(" org.h2.Driver" )
92- } else if (dbUrl.contains(" oracle" )) {
93- hikariConfig.setDriverClassName(" oracle.jdbc.OracleDriver" )
94- }
95- // For other databases, HikariCP will try to auto-detect the driver
73+ /**
74+ * Create a transactor that wraps an existing JDBC Connection.
75+ * Strategy.void ensures Doobie does not interfere with Lift's transaction management.
76+ */
77+ private def transactorFromConnection (conn : java.sql.Connection ): Transactor [IO ] = {
78+ val xa = Transactor .fromConnection[IO ].apply(conn, None )
79+ xa.copy(strategy0 = Strategy .void)
80+ }
9681
97- // Create the transactor - this allocates the pool immediately
98- // We use unsafeRunSync here because we need a stable, long-lived transactor
99- HikariTransactor .fromHikariConfig[IO ](hikariConfig)
100- .allocated
101- .map(_._1) // Get the transactor, discard the finalizer (pool lives for app lifetime)
102- .unsafeRunSync()
82+ /**
83+ * Try to get the current Lift request's Connection.
84+ * Uses DB.currentConnection which peeks at the DynoVar without
85+ * triggering reference counting or creating a new connection.
86+ * Returns Some(connection) if inside a Lift HTTP request context,
87+ * None otherwise (background tasks, schedulers, tests without request context).
88+ */
89+ private def liftCurrentConnection : Option [java.sql.Connection ] = {
90+ // DB.currentConnection returns Box[SuperConnection]
91+ // SuperConnection has implicit conversion to java.sql.Connection
92+ DB .currentConnection match {
93+ case Full (superConn) =>
94+ val conn : java.sql.Connection = superConn.connection
95+ if (! conn.isClosed) Some (conn) else None
96+ case _ => None
97+ }
10398 }
10499
105100 /**
106- * Run a Doobie query synchronously using the dedicated Doobie connection pool .
101+ * Run a Doobie query synchronously, sharing Lift's transaction when available .
107102 *
108- * This uses a completely separate HikariCP pool from Lift's pool,
109- * ensuring no interference between Doobie and Lift's connection management.
103+ * When called within a Lift HTTP request context:
104+ * - Uses the SAME Connection that Lift holds for the current request
105+ * - Doobie query participates in Lift's transaction (same commit/rollback)
106+ * - Can see uncommitted Lift writes (same database session)
107+ *
108+ * When called outside a Lift request context (background tasks, schedulers):
109+ * - Falls back to Lift's shared HikariCP pool (separate connection)
110110 *
111111 * @param query The Doobie ConnectionIO query to execute
112112 * @return The query result
113113 */
114114 def runQuery [A ](query : ConnectionIO [A ]): A = {
115- query.transact(transactor).unsafeRunSync()
115+ liftCurrentConnection match {
116+ case Some (conn) =>
117+ // Inside Lift request: use the same connection for transaction unification
118+ query.transact(transactorFromConnection(conn)).unsafeRunSync()
119+ case None =>
120+ // Outside Lift request: fallback to shared pool
121+ logger.debug(" DoobieUtil.runQuery: No Lift request context, using fallback pool transactor" )
122+ query.transact(fallbackTransactor).unsafeRunSync()
123+ }
116124 }
117125
118126 /**
119127 * Run a Doobie query asynchronously, returning a Future.
128+ * Note: async queries always use the fallback pool transactor because
129+ * Lift's request connection may not be available on a different thread.
120130 *
121131 * @param query The Doobie ConnectionIO query to execute
122132 * @param ec ExecutionContext for the Future
123133 * @return Future containing the query result
124134 */
125135 def runQueryAsync [A ](query : ConnectionIO [A ])(implicit ec : ExecutionContext ): Future [A ] = {
126- query.transact(transactor ).unsafeToFuture()
136+ query.transact(fallbackTransactor ).unsafeToFuture()
127137 }
128138
129139 /**
130140 * Run a Doobie query and return an IO.
131- * Useful when you want to compose with other cats-effect operations.
141+ * Note: IO queries always use the fallback pool transactor because
142+ * the IO may be evaluated outside the Lift request context.
132143 *
133144 * @param query The Doobie ConnectionIO query to execute
134145 * @return IO containing the query result
135146 */
136147 def runQueryIO [A ](query : ConnectionIO [A ]): IO [A ] = {
137- query.transact(transactor )
148+ query.transact(fallbackTransactor )
138149 }
139150
140151 /**
0 commit comments