Skip to main content

roomrs_core/
handle.rs

1//! 동기 실행 표면 — `SyncHandle` · `Tx` · `SqlContext` (명세 §5.0/§5.5/§5.7/§5.9)
2
3use crate::database::DatabaseInner;
4use crate::error::{Error, Result};
5use crate::row::FromRow;
6use rusqlite::types::FromSql;
7use rusqlite::{Connection, Params};
8use std::sync::Arc;
9
10/// 커넥션 하나 위에서의 공통 쿼리 구현 — 풀 체크아웃과 tx가 공유
11mod on_conn {
12    use super::*;
13
14    /// 쓰기 실행 — 영향 행 수 반환
15    pub(super) fn execute<P: Params>(conn: &Connection, sql: &str, params: P) -> Result<u64> {
16        let n = conn.execute(sql, params)?;
17        Ok(n as u64)
18    }
19
20    /// INSERT 실행 — 새 rowid 반환 (명세 §12c)
21    pub(super) fn insert<P: Params>(conn: &Connection, sql: &str, params: P) -> Result<i64> {
22        if conn.execute(sql, params)? == 0 {
23            return Err(Error::NotFound);
24        }
25        Ok(conn.last_insert_rowid())
26    }
27
28    /// N건 조회
29    pub(super) fn query_all<T: FromRow, P: Params>(
30        conn: &Connection,
31        sql: &str,
32        params: P,
33    ) -> Result<Vec<T>> {
34        let mut stmt = conn.prepare(sql)?;
35        let rows = stmt.query_map(params, |r| T::from_row(r))?;
36        let mut out = Vec::new();
37        for row in rows {
38            out.push(row?);
39        }
40        Ok(out)
41    }
42
43    /// 0~1건 조회
44    pub(super) fn query_optional<T: FromRow, P: Params>(
45        conn: &Connection,
46        sql: &str,
47        params: P,
48    ) -> Result<Option<T>> {
49        let mut stmt = conn.prepare(sql)?;
50        let mut rows = stmt.query(params)?;
51        match rows.next()? {
52            Some(row) => Ok(Some(T::from_row(row)?)),
53            None => Ok(None),
54        }
55    }
56
57    /// 스칼라 1건 조회 — 0건이면 NotFound
58    pub(super) fn query_scalar<T: FromSql, P: Params>(
59        conn: &Connection,
60        sql: &str,
61        params: P,
62    ) -> Result<T> {
63        let mut stmt = conn.prepare(sql)?;
64        let mut rows = stmt.query(params)?;
65        match rows.next()? {
66            Some(row) => Ok(row.get(0)?),
67            None => Err(Error::NotFound),
68        }
69    }
70}
71
72// ─────────────────────── SqlContext ───────────────────────
73
74/// DAO 공용 실행 컨텍스트 (명세 §5.9) —
75/// 풀-바운드(`SyncHandle`: 호출마다 통합 풀 체크아웃)와
76/// tx-바운드(`Tx`: 트랜잭션 커넥션 고정)를 하나의 trait로 통일한다.
77pub trait SqlContext {
78    /// 쓰기 실행 — 영향 행 수
79    fn ctx_execute<P: Params>(&self, sql: &str, params: P) -> Result<u64>;
80    /// INSERT — 새 rowid
81    fn ctx_insert<P: Params>(&self, sql: &str, params: P) -> Result<i64>;
82    /// N건 조회
83    fn ctx_query_all<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Vec<T>>;
84    /// 0~1건 조회
85    fn ctx_query_optional<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Option<T>>;
86    /// 정확히 1건 조회 (0건 = NotFound)
87    fn ctx_query_one<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<T> {
88        self.ctx_query_optional(sql, params)?.ok_or(Error::NotFound)
89    }
90    /// 스칼라 1건 조회
91    fn ctx_query_scalar<T: FromSql, P: Params>(&self, sql: &str, params: P) -> Result<T>;
92
93    /// 트랜잭션 실행 (명세 §5.9 한 메커니즘) —
94    /// 풀-바운드 = BEGIN/COMMIT, tx-바운드 = SAVEPOINT(중첩)
95    fn ctx_transaction<R>(&self, f: impl FnOnce(&Tx<'_>) -> Result<R>) -> Result<R>;
96}
97
98/// 참조 위임 — `&Tx` 도 컨텍스트로 쓸 수 있게
99impl<C: SqlContext> SqlContext for &C {
100    fn ctx_execute<P: Params>(&self, sql: &str, params: P) -> Result<u64> {
101        (**self).ctx_execute(sql, params)
102    }
103    fn ctx_insert<P: Params>(&self, sql: &str, params: P) -> Result<i64> {
104        (**self).ctx_insert(sql, params)
105    }
106    fn ctx_query_all<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Vec<T>> {
107        (**self).ctx_query_all(sql, params)
108    }
109    fn ctx_query_optional<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Option<T>> {
110        (**self).ctx_query_optional(sql, params)
111    }
112    fn ctx_query_scalar<T: FromSql, P: Params>(&self, sql: &str, params: P) -> Result<T> {
113        (**self).ctx_query_scalar(sql, params)
114    }
115    fn ctx_transaction<R>(&self, f: impl FnOnce(&Tx<'_>) -> Result<R>) -> Result<R> {
116        (**self).ctx_transaction(f)
117    }
118}
119
120// ─────────────────────── SyncHandle ───────────────────────
121
122/// 동기 핸들 — `db.run_sync()` 반환 타입 (명세 §5.0).
123/// 모든 작업은 read/write 통합 풀에서 커넥션을 체크아웃한다.
124///
125/// With the `live` feature, SQL classification is conservative. `PRAGMA`
126/// statements, including read-only forms, trigger full invalidation because
127/// SQLite state-changing forms cannot be distinguished reliably by the parser.
128#[derive(Clone, Copy)]
129pub struct SyncHandle<'db> {
130    pub(crate) inner: &'db Arc<DatabaseInner>,
131}
132
133impl<'db> SyncHandle<'db> {
134    /// 쓰기 실행 (명세 §5.7)
135    pub fn execute<P: Params>(&self, sql: &str, params: P) -> Result<u64> {
136        self.ctx_execute(sql, params)
137    }
138
139    /// 정확히 1건 조회 — 0건이면 `Error::NotFound`
140    pub fn query_one<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<T> {
141        self.ctx_query_one(sql, params)
142    }
143
144    /// 0~1건 조회
145    pub fn query_optional<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Option<T>> {
146        self.ctx_query_optional(sql, params)
147    }
148
149    /// 스칼라 1건 조회
150    pub fn query_scalar<T: FromSql, P: Params>(&self, sql: &str, params: P) -> Result<T> {
151        self.ctx_query_scalar(sql, params)
152    }
153
154    /// N건 조회
155    pub fn query_all<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Vec<T>> {
156        self.ctx_query_all(sql, params)
157    }
158
159    /// 클로저 트랜잭션 (명세 §5.5) — 에러 시 롤백, 성공 시 커밋
160    pub fn transaction<R>(&self, f: impl FnOnce(&mut Tx<'_>) -> Result<R>) -> Result<R> {
161        let mut tx = self.begin()?;
162        match f(&mut tx) {
163            Ok(v) => {
164                tx.commit()?;
165                Ok(v)
166            }
167            Err(e) => {
168                // 롤백 실패는 원인 에러를 대체하지 않는다 (L-1) —
169                // 실패 시 rollback()이 Tx를 open 상태로 drop해 롤백을 재시도한다
170                let _ = tx.rollback();
171                Err(e)
172            }
173        }
174    }
175
176    /// RAII 트랜잭션 — drop 시 미커밋이면 롤백 (명세 §5.5)
177    pub fn begin(&self) -> Result<Tx<'db>> {
178        let guard = self.inner.pool.connections.acquire()?;
179        // WAL에서 deferred BEGIN은 read→write 승격 시 SQLITE_BUSY_SNAPSHOT을
180        // 반환해 busy_timeout을 우회한다 — 쓰기 트랜잭션은 IMMEDIATE로 시작 (H-3)
181        guard.conn().execute_batch("BEGIN IMMEDIATE")?;
182        log::debug!("transaction begin");
183        Ok(Tx {
184            inner: self.inner,
185            guard,
186            open: true,
187            sp_depth: std::cell::Cell::new(0),
188            #[cfg(feature = "live")]
189            pending: std::sync::Mutex::new(vec![TxPending::default()]),
190        })
191    }
192
193    /// Runs a closure with one exclusively checked-out read/write connection.
194    ///
195    /// Do not acquire another connection from the same database inside the
196    /// closure when every pooled connection may already be checked out. Doing
197    /// so can block indefinitely (or until the configured queue timeout)
198    /// because the outer checkout cannot return.
199    ///
200    /// With the `live` feature, direct writes are observed through SQLite's
201    /// update hook. SQLite does not invoke that hook for every change: notably,
202    /// truncate-optimized `DELETE` statements and changes to `WITHOUT ROWID`
203    /// tables may be missed. Use the handle's SQL methods when live-query
204    /// invalidation must be guaranteed.
205    ///
206    /// Installing an SQLite `preupdate_hook` replaces roomrs' live-query hook on
207    /// that connection. With the `live` feature, call [`Self::rearm_hooks`]
208    /// afterwards.
209    pub fn with_connection<R>(&self, f: impl FnOnce(&Connection) -> Result<R>) -> Result<R> {
210        let guard = self.inner.pool.connections.acquire()?;
211        #[cfg(feature = "live")]
212        let _hook_capture = self.inner.begin_hook_capture();
213        let out = f(guard.conn());
214        #[cfg(feature = "live")]
215        {
216            let capture = self.inner.take_hook_capture();
217            if !capture.changes.is_empty() {
218                self.inner.tracker.invalidate_changes(capture.changes);
219            } else if !capture.tables.is_empty() {
220                self.inner.tracker.invalidate(Some(capture.tables));
221            }
222        }
223        out
224    }
225
226    /// Reinstalls roomrs' update hook on every pooled connection.
227    ///
228    /// Waits for all checked-out connections to return, then updates the
229    /// complete pool while new checkouts remain blocked.
230    #[cfg(feature = "live")]
231    pub fn rearm_hooks(&self) -> Result<()> {
232        self.inner.pool.connections.for_each_connection(|conn| {
233            crate::database::install_preupdate_hook(conn, Arc::clone(&self.inner.hook_columns))
234        })
235    }
236}
237
238// ─────────────────────── watch (live) ───────────────────────
239
240/// 라이브 쿼리 생성 공용 구현 (명세 §5.7) — Sync/Async 핸들이 공유
241#[cfg(feature = "live")]
242pub(crate) mod watch_impl {
243    use super::*;
244    use crate::live::{InvalidationFilter, LiveQuery, OwnedParams, extract_tables};
245    use rusqlite::ToSql;
246    use std::collections::HashSet;
247
248    /// 파라미터 변환 실패를 첫 emit 에러로 바꾸는 생성 헬퍼
249    fn make<T: Clone + Send + 'static>(
250        inner: &Arc<DatabaseInner>,
251        sql: String,
252        params: Result<OwnedParams>,
253        tables: Option<HashSet<String>>,
254        run: impl Fn(&Connection, &str, &OwnedParams) -> Result<T> + Send + Sync + 'static,
255    ) -> LiveQuery<T> {
256        let tracker = Arc::clone(&inner.tracker);
257        match params {
258            Ok(p) => LiveQuery::new(tracker, sql, p, tables, run),
259            Err(e) => {
260                // 변환 실패 — 빈 의존으로 등록하고 첫 emit이 에러가 되게 한다
261                let msg = e.to_string();
262                LiveQuery::new(
263                    tracker,
264                    sql,
265                    OwnedParams::None,
266                    Some(HashSet::new()),
267                    move |_, _, _| Err(Error::Config(msg.clone())),
268                )
269            }
270        }
271    }
272
273    /// N건 라이브
274    pub(crate) fn watch_all<T: FromRow + Clone + Send + 'static>(
275        inner: &Arc<DatabaseInner>,
276        sql: &str,
277        params: Result<OwnedParams>,
278        tables: Option<HashSet<String>>,
279    ) -> LiveQuery<Vec<T>> {
280        let tables = tables.or_else(|| extract_tables(sql));
281        make(
282            inner,
283            sql.to_string(),
284            params,
285            tables,
286            crate::live::query_all_owned::<T>,
287        )
288    }
289
290    /// 0~1건 라이브
291    pub(crate) fn watch_optional<T: FromRow + Clone + Send + 'static>(
292        inner: &Arc<DatabaseInner>,
293        sql: &str,
294        params: Result<OwnedParams>,
295        tables: Option<HashSet<String>>,
296    ) -> LiveQuery<Option<T>> {
297        let tables = tables.or_else(|| extract_tables(sql));
298        make(
299            inner,
300            sql.to_string(),
301            params,
302            tables,
303            crate::live::query_optional_owned::<T>,
304        )
305    }
306
307    /// 스칼라 라이브
308    pub(crate) fn watch_scalar<T: FromSql + Clone + Send + 'static>(
309        inner: &Arc<DatabaseInner>,
310        sql: &str,
311        params: Result<OwnedParams>,
312        tables: Option<HashSet<String>>,
313    ) -> LiveQuery<T> {
314        let tables = tables.or_else(|| extract_tables(sql));
315        make(
316            inner,
317            sql.to_string(),
318            params,
319            tables,
320            crate::live::query_scalar_owned::<T>,
321        )
322    }
323
324    pub(crate) fn watch_scalar_filtered<T: FromSql + Clone + Send + 'static>(
325        inner: &Arc<DatabaseInner>,
326        sql: &str,
327        params: Result<OwnedParams>,
328        filter: InvalidationFilter,
329    ) -> LiveQuery<T> {
330        let tables = Some(HashSet::from([filter.table_name().to_string()]));
331        let tracker = Arc::clone(&inner.tracker);
332        match params {
333            Ok(p) => LiveQuery::new_filtered(
334                tracker,
335                sql.to_string(),
336                p,
337                tables,
338                Some(filter),
339                crate::live::query_scalar_owned::<T>,
340            ),
341            Err(e) => LiveQuery::new(
342                tracker,
343                sql.to_string(),
344                OwnedParams::None,
345                Some(HashSet::new()),
346                move |_, _, _| Err(Error::Config(e.to_string())),
347            ),
348        }
349    }
350
351    /// DAO 힌트 테이블 목록 → 집합 (빈 목록 = 런타임 추출 위임)
352    pub(crate) fn tables_from_hint(hint: &[&str]) -> Option<HashSet<String>> {
353        if hint.is_empty() {
354            None
355        } else {
356            Some(hint.iter().map(|s| s.to_string()).collect())
357        }
358    }
359
360    /// 빌린 positional 파라미터 변환 래퍼
361    pub(crate) fn params_from_dyn(params: &[&dyn ToSql]) -> Result<OwnedParams> {
362        OwnedParams::from_dyn(params)
363    }
364
365    /// 매크로 생성 코드용 — 명명 파라미터 소유 변환 결과 래핑
366    pub(crate) fn params_named(
367        pairs: Result<Vec<(String, rusqlite::types::Value)>>,
368    ) -> Result<OwnedParams> {
369        pairs.map(OwnedParams::Named)
370    }
371}
372
373#[cfg(feature = "live")]
374impl SyncHandle<'_> {
375    /// N건 라이브 쿼리 (명세 §5.7) — 의존 추출 실패 시 첫 수신이 `UnknownDependencies`
376    pub fn watch_all<T: FromRow + Clone + Send + 'static>(
377        &self,
378        sql: &str,
379        params: &[&dyn rusqlite::ToSql],
380    ) -> crate::live::LiveQuery<Vec<T>> {
381        watch_impl::watch_all(self.inner, sql, watch_impl::params_from_dyn(params), None)
382    }
383
384    /// 0~1건 라이브 쿼리
385    pub fn watch_optional<T: FromRow + Clone + Send + 'static>(
386        &self,
387        sql: &str,
388        params: &[&dyn rusqlite::ToSql],
389    ) -> crate::live::LiveQuery<Option<T>> {
390        watch_impl::watch_optional(self.inner, sql, watch_impl::params_from_dyn(params), None)
391    }
392
393    /// 스칼라 라이브 쿼리 (COUNT 등)
394    pub fn watch_scalar<T: FromSql + Clone + Send + 'static>(
395        &self,
396        sql: &str,
397        params: &[&dyn rusqlite::ToSql],
398    ) -> crate::live::LiveQuery<T> {
399        watch_impl::watch_scalar(self.inner, sql, watch_impl::params_from_dyn(params), None)
400    }
401
402    pub fn watch_scalar_filtered<T: FromSql + Clone + Send + 'static>(
403        &self,
404        sql: &str,
405        params: &[&dyn rusqlite::ToSql],
406        filter: crate::live::InvalidationFilter,
407    ) -> crate::live::LiveQuery<T> {
408        watch_impl::watch_scalar_filtered(
409            self.inner,
410            sql,
411            watch_impl::params_from_dyn(params),
412            filter,
413        )
414    }
415}
416
417/// roomrs-async 전용 watch 진입점 — 직접 사용 금지
418#[cfg(feature = "live")]
419#[doc(hidden)]
420impl DatabaseInner {
421    pub fn __watch_all_dyn<T: FromRow + Clone + Send + 'static>(
422        self: &Arc<Self>,
423        sql: &str,
424        params: &[&dyn rusqlite::ToSql],
425    ) -> crate::live::LiveQuery<Vec<T>> {
426        watch_impl::watch_all(self, sql, watch_impl::params_from_dyn(params), None)
427    }
428    pub fn __watch_optional_dyn<T: FromRow + Clone + Send + 'static>(
429        self: &Arc<Self>,
430        sql: &str,
431        params: &[&dyn rusqlite::ToSql],
432    ) -> crate::live::LiveQuery<Option<T>> {
433        watch_impl::watch_optional(self, sql, watch_impl::params_from_dyn(params), None)
434    }
435    pub fn __watch_scalar_dyn<T: FromSql + Clone + Send + 'static>(
436        self: &Arc<Self>,
437        sql: &str,
438        params: &[&dyn rusqlite::ToSql],
439    ) -> crate::live::LiveQuery<T> {
440        watch_impl::watch_scalar(self, sql, watch_impl::params_from_dyn(params), None)
441    }
442    pub fn __watch_all_named<T: FromRow + Clone + Send + 'static>(
443        self: &Arc<Self>,
444        sql: &'static str,
445        params: Result<Vec<(String, rusqlite::types::Value)>>,
446        tables: &[&str],
447    ) -> crate::live::LiveQuery<Vec<T>> {
448        watch_impl::watch_all(
449            self,
450            sql,
451            watch_impl::params_named(params),
452            watch_impl::tables_from_hint(tables),
453        )
454    }
455    pub fn __watch_optional_named<T: FromRow + Clone + Send + 'static>(
456        self: &Arc<Self>,
457        sql: &'static str,
458        params: Result<Vec<(String, rusqlite::types::Value)>>,
459        tables: &[&str],
460    ) -> crate::live::LiveQuery<Option<T>> {
461        watch_impl::watch_optional(
462            self,
463            sql,
464            watch_impl::params_named(params),
465            watch_impl::tables_from_hint(tables),
466        )
467    }
468    pub fn __watch_scalar_named<T: FromSql + Clone + Send + 'static>(
469        self: &Arc<Self>,
470        sql: &'static str,
471        params: Result<Vec<(String, rusqlite::types::Value)>>,
472        tables: &[&str],
473    ) -> crate::live::LiveQuery<T> {
474        watch_impl::watch_scalar(
475            self,
476            sql,
477            watch_impl::params_named(params),
478            watch_impl::tables_from_hint(tables),
479        )
480    }
481}
482
483/// DAO watch 컨텍스트 (명세 §5.6) — 풀-바운드 전용.
484/// Tx 구현은 "트랜잭션에서 라이브 불가" 에러를 첫 emit으로 전달한다.
485#[cfg(feature = "live")]
486pub trait WatchContext {
487    /// 매크로 생성 코드 전용 — 직접 사용 금지
488    #[doc(hidden)]
489    fn ctx_watch_all_named<T: FromRow + Clone + Send + 'static>(
490        &self,
491        sql: &'static str,
492        params: Result<Vec<(String, rusqlite::types::Value)>>,
493        tables: &[&str],
494    ) -> crate::live::LiveQuery<Vec<T>>;
495    #[doc(hidden)]
496    fn ctx_watch_optional_named<T: FromRow + Clone + Send + 'static>(
497        &self,
498        sql: &'static str,
499        params: Result<Vec<(String, rusqlite::types::Value)>>,
500        tables: &[&str],
501    ) -> crate::live::LiveQuery<Option<T>>;
502    #[doc(hidden)]
503    fn ctx_watch_scalar_named<T: FromSql + Clone + Send + 'static>(
504        &self,
505        sql: &'static str,
506        params: Result<Vec<(String, rusqlite::types::Value)>>,
507        tables: &[&str],
508    ) -> crate::live::LiveQuery<T>;
509}
510
511#[cfg(feature = "live")]
512impl<C: WatchContext> WatchContext for &C {
513    fn ctx_watch_all_named<T: FromRow + Clone + Send + 'static>(
514        &self,
515        sql: &'static str,
516        params: Result<Vec<(String, rusqlite::types::Value)>>,
517        tables: &[&str],
518    ) -> crate::live::LiveQuery<Vec<T>> {
519        (**self).ctx_watch_all_named(sql, params, tables)
520    }
521    fn ctx_watch_optional_named<T: FromRow + Clone + Send + 'static>(
522        &self,
523        sql: &'static str,
524        params: Result<Vec<(String, rusqlite::types::Value)>>,
525        tables: &[&str],
526    ) -> crate::live::LiveQuery<Option<T>> {
527        (**self).ctx_watch_optional_named(sql, params, tables)
528    }
529    fn ctx_watch_scalar_named<T: FromSql + Clone + Send + 'static>(
530        &self,
531        sql: &'static str,
532        params: Result<Vec<(String, rusqlite::types::Value)>>,
533        tables: &[&str],
534    ) -> crate::live::LiveQuery<T> {
535        (**self).ctx_watch_scalar_named(sql, params, tables)
536    }
537}
538
539#[cfg(feature = "live")]
540impl WatchContext for SyncHandle<'_> {
541    fn ctx_watch_all_named<T: FromRow + Clone + Send + 'static>(
542        &self,
543        sql: &'static str,
544        params: Result<Vec<(String, rusqlite::types::Value)>>,
545        tables: &[&str],
546    ) -> crate::live::LiveQuery<Vec<T>> {
547        watch_impl::watch_all(
548            self.inner,
549            sql,
550            watch_impl::params_named(params),
551            watch_impl::tables_from_hint(tables),
552        )
553    }
554    fn ctx_watch_optional_named<T: FromRow + Clone + Send + 'static>(
555        &self,
556        sql: &'static str,
557        params: Result<Vec<(String, rusqlite::types::Value)>>,
558        tables: &[&str],
559    ) -> crate::live::LiveQuery<Option<T>> {
560        watch_impl::watch_optional(
561            self.inner,
562            sql,
563            watch_impl::params_named(params),
564            watch_impl::tables_from_hint(tables),
565        )
566    }
567    fn ctx_watch_scalar_named<T: FromSql + Clone + Send + 'static>(
568        &self,
569        sql: &'static str,
570        params: Result<Vec<(String, rusqlite::types::Value)>>,
571        tables: &[&str],
572    ) -> crate::live::LiveQuery<T> {
573        watch_impl::watch_scalar(
574            self.inner,
575            sql,
576            watch_impl::params_named(params),
577            watch_impl::tables_from_hint(tables),
578        )
579    }
580}
581
582#[cfg(feature = "live")]
583impl WatchContext for Tx<'_> {
584    /// 트랜잭션 컨텍스트 = 라이브 불가 — 첫 emit이 에러 (graceful)
585    fn ctx_watch_all_named<T: FromRow + Clone + Send + 'static>(
586        &self,
587        sql: &'static str,
588        _params: Result<Vec<(String, rusqlite::types::Value)>>,
589        _tables: &[&str],
590    ) -> crate::live::LiveQuery<Vec<T>> {
591        watch_impl::watch_all(
592            self.inner,
593            sql,
594            Err(Error::Config(
595                "트랜잭션 컨텍스트에서는 라이브 쿼리를 만들 수 없습니다".into(),
596            )),
597            Some(std::collections::HashSet::new()),
598        )
599    }
600    fn ctx_watch_optional_named<T: FromRow + Clone + Send + 'static>(
601        &self,
602        sql: &'static str,
603        _params: Result<Vec<(String, rusqlite::types::Value)>>,
604        _tables: &[&str],
605    ) -> crate::live::LiveQuery<Option<T>> {
606        watch_impl::watch_optional(
607            self.inner,
608            sql,
609            Err(Error::Config(
610                "트랜잭션 컨텍스트에서는 라이브 쿼리를 만들 수 없습니다".into(),
611            )),
612            Some(std::collections::HashSet::new()),
613        )
614    }
615    fn ctx_watch_scalar_named<T: FromSql + Clone + Send + 'static>(
616        &self,
617        sql: &'static str,
618        _params: Result<Vec<(String, rusqlite::types::Value)>>,
619        _tables: &[&str],
620    ) -> crate::live::LiveQuery<T> {
621        watch_impl::watch_scalar(
622            self.inner,
623            sql,
624            Err(Error::Config(
625                "트랜잭션 컨텍스트에서는 라이브 쿼리를 만들 수 없습니다".into(),
626            )),
627            Some(std::collections::HashSet::new()),
628        )
629    }
630}
631
632impl SqlContext for SyncHandle<'_> {
633    fn ctx_execute<P: Params>(&self, sql: &str, params: P) -> Result<u64> {
634        let guard = self.inner.pool.connections.acquire()?;
635        #[cfg(feature = "live")]
636        let _hook_capture = self.inner.begin_hook_capture();
637        let out = self
638            .inner
639            .log_query(sql, || on_conn::execute(guard.conn(), sql, params));
640        // 자동 커밋 write — 즉시 방출 (명세 §9.2). OR FAIL/RAISE(FAIL)은 문장
641        // 에러여도 선행 행 변경이 영속되므로 성패 무관 방출 (R3-1)
642        #[cfg(feature = "live")]
643        self.inner.emit_after_write(sql);
644        out
645    }
646    fn ctx_insert<P: Params>(&self, sql: &str, params: P) -> Result<i64> {
647        let guard = self.inner.pool.connections.acquire()?;
648        #[cfg(feature = "live")]
649        let _hook_capture = self.inner.begin_hook_capture();
650        let out = self
651            .inner
652            .log_query(sql, || on_conn::insert(guard.conn(), sql, params));
653        // 성패 무관 방출 — OR FAIL 부분 적용 대비 (R3-1)
654        #[cfg(feature = "live")]
655        self.inner.emit_after_write(sql);
656        out
657    }
658    fn ctx_query_all<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Vec<T>> {
659        let guard = self.inner.pool.connections.acquire()?;
660        #[cfg(feature = "live")]
661        let _hook_capture = self.inner.begin_hook_capture();
662        let out = self
663            .inner
664            .log_query(sql, || on_conn::query_all(guard.conn(), sql, params));
665        #[cfg(feature = "live")]
666        self.inner.emit_after_write(sql);
667        out
668    }
669    fn ctx_query_optional<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Option<T>> {
670        let guard = self.inner.pool.connections.acquire()?;
671        #[cfg(feature = "live")]
672        let _hook_capture = self.inner.begin_hook_capture();
673        let out = self
674            .inner
675            .log_query(sql, || on_conn::query_optional(guard.conn(), sql, params));
676        #[cfg(feature = "live")]
677        self.inner.emit_after_write(sql);
678        out
679    }
680    fn ctx_query_scalar<T: FromSql, P: Params>(&self, sql: &str, params: P) -> Result<T> {
681        let guard = self.inner.pool.connections.acquire()?;
682        #[cfg(feature = "live")]
683        let _hook_capture = self.inner.begin_hook_capture();
684        let out = self
685            .inner
686            .log_query(sql, || on_conn::query_scalar(guard.conn(), sql, params));
687        #[cfg(feature = "live")]
688        self.inner.emit_after_write(sql);
689        out
690    }
691    /// 풀-바운드 트랜잭션 — BEGIN/COMMIT (에러 시 롤백)
692    fn ctx_transaction<R>(&self, f: impl FnOnce(&Tx<'_>) -> Result<R>) -> Result<R> {
693        let tx = self.begin()?;
694        match f(&tx) {
695            Ok(v) => {
696                tx.commit()?;
697                Ok(v)
698            }
699            Err(e) => {
700                // 롤백 실패는 원인 에러를 대체하지 않는다 (L-1) — drop 경로가 재시도
701                let _ = tx.rollback();
702                Err(e)
703            }
704        }
705    }
706}
707
708// ─────────────────────── Tx ───────────────────────
709
710/// A synchronous transaction bound to its checked-out connection and thread.
711///
712/// `Tx` is intentionally not `Send`: SQLite transaction work and rollback
713/// must remain on the checkout owner thread. Use the async handle to schedule
714/// database work instead of moving a synchronous transaction between threads.
715/// An uncommitted transaction is rolled back when dropped.
716/// With the `live` feature, `PRAGMA` statements conservatively accumulate full
717/// invalidation, including read-only forms.
718pub struct Tx<'db> {
719    inner: &'db Arc<DatabaseInner>,
720    guard: crate::pool::ConnectionGuard<'db>,
721    open: bool,
722    /// savepoint 중첩 깊이 — 이름 생성용 (명세 §5.9 중첩)
723    sp_depth: std::cell::Cell<u32>,
724    /// tx 동안 누적된 무효화 대상 — savepoint 깊이별 레벨 스택 (L-8),
725    /// commit 성공 후에만 방출 (명세 §9.2)
726    #[cfg(feature = "live")]
727    pending: std::sync::Mutex<Vec<TxPending>>,
728}
729
730/// tx-로컬 무효화 누적 상태 (savepoint 레벨 단위)
731#[cfg(feature = "live")]
732#[derive(Default)]
733struct TxPending {
734    /// 파싱 실패 발생 = 전체 무효화 필요
735    all: bool,
736    tables: std::collections::HashSet<String>,
737    changes: Vec<crate::live::TableChange>,
738}
739
740#[cfg(feature = "live")]
741impl Tx<'_> {
742    /// write 문장 1건의 무효화 대상 누적 — 문장 파싱 ∪ 훅 (현재 savepoint 레벨에).
743    /// 읽기 전용 문장은 문장 기반 누적을 하지 않는다 (L-2) — 훅 수집분만 병합
744    fn collect_write(&self, sql: &str) {
745        let capture = self.inner.take_hook_capture();
746        let mut levels = self
747            .pending
748            .lock()
749            .unwrap_or_else(std::sync::PoisonError::into_inner);
750        // begin이 기본 레벨 1개를 보장 — 비어 있으면 방어적으로 무시
751        let Some(p) = levels.last_mut() else { return };
752        p.tables.extend(capture.tables);
753        p.changes.extend(capture.changes);
754        match crate::live::extract_write_tables(sql) {
755            crate::live::WriteTables::ReadOnly => {}
756            crate::live::WriteTables::Tables(t) => p.tables.extend(t),
757            crate::live::WriteTables::Unknown => p.all = true,
758        }
759    }
760
761    /// 부분 실패 배치 등 결과를 알 수 없는 write — 훅 수집 ∪ 전체 무효화를
762    /// 현재 레벨에 보수 누적한다 (R2-3). 커밋되지 않으면 방출되지 않으므로 무해
763    fn collect_write_all(&self) {
764        let capture = self.inner.take_hook_capture();
765        let mut levels = self
766            .pending
767            .lock()
768            .unwrap_or_else(std::sync::PoisonError::into_inner);
769        let Some(p) = levels.last_mut() else { return };
770        p.tables.extend(capture.tables);
771        p.changes.extend(capture.changes);
772        p.all = true;
773    }
774
775    /// commit 성공 후 방출 — 남은 레벨 전부 병합
776    fn emit_pending(&self) {
777        let levels = std::mem::take(
778            &mut *self
779                .pending
780                .lock()
781                .unwrap_or_else(std::sync::PoisonError::into_inner),
782        );
783        let mut all = false;
784        let mut tables = std::collections::HashSet::new();
785        let mut changes = Vec::new();
786        for l in levels {
787            all |= l.all;
788            tables.extend(l.tables);
789            changes.extend(l.changes);
790        }
791        if all {
792            self.inner.tracker.invalidate(None);
793        } else {
794            let changed_tables: std::collections::HashSet<String> = changes
795                .iter()
796                .map(|change: &crate::live::TableChange| change.table.clone())
797                .collect();
798            if !changes.is_empty() {
799                self.inner.tracker.invalidate_changes(changes);
800            }
801            tables.retain(|table| !changed_tables.contains(table));
802            if !tables.is_empty() {
803                self.inner.tracker.invalidate(Some(tables));
804            }
805        }
806    }
807}
808
809impl Tx<'_> {
810    /// 트랜잭션 커넥션 — 크레이트 내부 전용 (마이그레이션 러너)
811    pub(crate) fn raw_conn(&self) -> &Connection {
812        self.guard.conn()
813    }
814
815    /// 커밋 — 이 Tx 소비. 성공 시 누적 무효화 방출 (명세 §9.2 — commit API 성공 반환 후)
816    pub fn commit(mut self) -> Result<()> {
817        self.guard.conn().execute_batch("COMMIT")?;
818        log::debug!("transaction commit");
819        self.open = false;
820        #[cfg(feature = "live")]
821        self.emit_pending();
822        Ok(())
823    }
824
825    /// 명시 롤백 — 이 Tx 소비
826    pub fn rollback(mut self) -> Result<()> {
827        self.guard.conn().execute_batch("ROLLBACK")?;
828        log::debug!("transaction rollback");
829        self.open = false;
830        Ok(())
831    }
832
833    /// 쓰기 실행 (트랜잭션 커넥션)
834    pub fn execute<P: Params>(&self, sql: &str, params: P) -> Result<u64> {
835        self.ctx_execute(sql, params)
836    }
837
838    /// 정확히 1건 조회
839    pub fn query_one<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<T> {
840        self.ctx_query_one(sql, params)
841    }
842
843    /// 0~1건 조회
844    pub fn query_optional<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Option<T>> {
845        self.ctx_query_optional(sql, params)
846    }
847
848    /// 스칼라 1건 조회
849    pub fn query_scalar<T: FromSql, P: Params>(&self, sql: &str, params: P) -> Result<T> {
850        self.ctx_query_scalar(sql, params)
851    }
852
853    /// N건 조회
854    pub fn query_all<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Vec<T>> {
855        self.ctx_query_all(sql, params)
856    }
857
858    /// 배치 실행 — 여러 문장 세미콜론 구분 (마이그레이션·일괄 write용).
859    ///
860    /// Batch writes participate in invalidation (M-2): affected tables are
861    /// collected per statement and emitted after the transaction commits. A
862    /// statement that cannot be parsed degrades to a conservative
863    /// full invalidation.
864    pub fn execute_batch(&self, sql: &str) -> Result<()> {
865        #[cfg(feature = "live")]
866        let _hook_capture = self.inner.begin_hook_capture();
867        let r = self.raw_conn().execute_batch(sql);
868        #[cfg(feature = "live")]
869        match &r {
870            // 배치 write도 커밋 시 무효화에 포함 (M-2)
871            Ok(()) => self.collect_write(sql),
872            // 부분 실패 — 선행 문장은 이미 적용됐을 수 있다: 사용자가 에러를
873            // 삼키고 커밋해도 무효화가 소실되지 않게 보수 누적 (R2-3)
874            Err(_) => self.collect_write_all(),
875        }
876        r?;
877        Ok(())
878    }
879
880    /// 중첩 트랜잭션 — SAVEPOINT (명세 §5.9).
881    /// 실패 시 savepoint까지만 롤백, 외부 트랜잭션은 계속된다.
882    /// 무효화 누적도 레벨 단위 — 롤백된 write는 방출하지 않는다 (L-8)
883    pub fn savepoint<R>(&self, f: impl FnOnce(&Tx<'_>) -> Result<R>) -> Result<R> {
884        let depth = self.sp_depth.get();
885        let name = format!("roomrs_sp_{depth}");
886        self.guard
887            .conn()
888            .execute_batch(&format!("SAVEPOINT {name}"))?;
889        self.sp_depth.set(depth + 1);
890        // savepoint 레벨 격리 — 롤백 시 이 레벨 무효화 대상을 폐기 (L-8)
891        #[cfg(feature = "live")]
892        self.pending
893            .lock()
894            .unwrap_or_else(std::sync::PoisonError::into_inner)
895            .push(TxPending::default());
896
897        let out = f(self);
898
899        self.sp_depth.set(depth);
900        // 레벨 회수 — 성공 시 부모로 병합, 실패 시 폐기 (L-8)
901        #[cfg(feature = "live")]
902        let level = self
903            .pending
904            .lock()
905            .unwrap_or_else(std::sync::PoisonError::into_inner)
906            .pop()
907            .unwrap_or_default();
908        match out {
909            Ok(v) => {
910                // RELEASE 실패 시에도 savepoint 데이터는 남아 있으므로 먼저 병합
911                #[cfg(feature = "live")]
912                {
913                    let mut levels = self
914                        .pending
915                        .lock()
916                        .unwrap_or_else(std::sync::PoisonError::into_inner);
917                    if let Some(parent) = levels.last_mut() {
918                        parent.all |= level.all;
919                        parent.tables.extend(level.tables);
920                        parent.changes.extend(level.changes);
921                    }
922                }
923                self.guard
924                    .conn()
925                    .execute_batch(&format!("RELEASE {name}"))?;
926                Ok(v)
927            }
928            Err(e) => {
929                // ROLLBACK TO는 savepoint를 유지하므로 RELEASE로 제거까지.
930                // 롤백 실패는 원인 에러를 대체하지 않는다 (M-3) — 로그 후 원본 반환
931                if let Err(re) = self
932                    .guard
933                    .conn()
934                    .execute_batch(&format!("ROLLBACK TO {name}; RELEASE {name}"))
935                {
936                    log::error!("savepoint rollback failed: {re}");
937                }
938                Err(e)
939            }
940        }
941    }
942}
943
944impl SqlContext for Tx<'_> {
945    fn ctx_execute<P: Params>(&self, sql: &str, params: P) -> Result<u64> {
946        #[cfg(feature = "live")]
947        let _hook_capture = self.inner.begin_hook_capture();
948        let out = self
949            .inner
950            .log_query(sql, || on_conn::execute(self.guard.conn(), sql, params));
951        // OR FAIL/RAISE(FAIL)은 문장 에러여도 선행 행 변경이 남는다 —
952        // 성패 무관 수집(롤백되면 방출 안 됨) (R3-1)
953        #[cfg(feature = "live")]
954        self.collect_write(sql);
955        out
956    }
957    fn ctx_insert<P: Params>(&self, sql: &str, params: P) -> Result<i64> {
958        #[cfg(feature = "live")]
959        let _hook_capture = self.inner.begin_hook_capture();
960        let out = self
961            .inner
962            .log_query(sql, || on_conn::insert(self.guard.conn(), sql, params));
963        // 성패 무관 수집 — OR FAIL 부분 적용 대비 (R3-1)
964        #[cfg(feature = "live")]
965        self.collect_write(sql);
966        out
967    }
968    fn ctx_query_all<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Vec<T>> {
969        // 쓰기 가능 SQL(INSERT…RETURNING 등)은 execute와 동일한 무효화 수집 (H-1).
970        // RETURNING은 첫 step에서 DML이 완결되므로 매핑 실패에도 write가 영속될
971        // 수 있다 — 성패 무관 수집(롤백되면 방출 안 됨) (R2-2)
972        #[cfg(feature = "live")]
973        let _hook_capture = self.inner.begin_hook_capture();
974        let out = self
975            .inner
976            .log_query(sql, || on_conn::query_all(self.guard.conn(), sql, params));
977        #[cfg(feature = "live")]
978        self.collect_write(sql);
979        out
980    }
981    fn ctx_query_optional<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Option<T>> {
982        // 쓰기 가능 SQL은 execute와 동일한 무효화 수집 (H-1).
983        // 매핑 실패에도 write가 영속될 수 있어 성패 무관 수집 (R2-2)
984        #[cfg(feature = "live")]
985        let _hook_capture = self.inner.begin_hook_capture();
986        let out = self.inner.log_query(sql, || {
987            on_conn::query_optional(self.guard.conn(), sql, params)
988        });
989        #[cfg(feature = "live")]
990        self.collect_write(sql);
991        out
992    }
993    fn ctx_query_scalar<T: FromSql, P: Params>(&self, sql: &str, params: P) -> Result<T> {
994        // 쓰기 가능 SQL은 execute와 동일한 무효화 수집 (H-1).
995        // 매핑 실패에도 write가 영속될 수 있어 성패 무관 수집 (R2-2)
996        #[cfg(feature = "live")]
997        let _hook_capture = self.inner.begin_hook_capture();
998        let out = self.inner.log_query(sql, || {
999            on_conn::query_scalar(self.guard.conn(), sql, params)
1000        });
1001        #[cfg(feature = "live")]
1002        self.collect_write(sql);
1003        out
1004    }
1005    /// tx-바운드 트랜잭션 = 중첩 savepoint (명세 §5.9)
1006    fn ctx_transaction<R>(&self, f: impl FnOnce(&Tx<'_>) -> Result<R>) -> Result<R> {
1007        self.savepoint(f)
1008    }
1009}
1010
1011impl Drop for Tx<'_> {
1012    /// 미커밋 drop = 롤백 — 실패해도 panic하지 않는다(커넥션 반납이 우선)
1013    fn drop(&mut self) {
1014        if self.open {
1015            log::debug!("transaction rollback (uncommitted drop)");
1016            let _ = self.guard.conn().execute_batch("ROLLBACK");
1017        }
1018    }
1019}