use crate::error::{Error, Result};
use crate::row::FromRow;
use rusqlite::types::Value;
use rusqlite::{Connection, ToSql};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
use std::time::Duration;
fn plock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| {
log::warn!("mutex poisoned in live-query state — recovering");
e.into_inner()
})
}
fn query_tables(q: &sqlparser::ast::Query, out: &mut HashSet<String>) -> bool {
use core::ops::ControlFlow;
use sqlparser::ast::{ObjectName, Query, TableFactor, Visit, Visitor};
struct Deps<'a> {
out: &'a mut HashSet<String>,
}
impl Visitor for Deps<'_> {
type Break = ();
fn pre_visit_query(&mut self, q: &Query) -> ControlFlow<()> {
if q.with.is_some() {
return ControlFlow::Break(());
}
ControlFlow::Continue(())
}
fn pre_visit_relation(&mut self, name: &ObjectName) -> ControlFlow<()> {
if let Some(last) = name.0.last() {
self.out.insert(last.value.clone());
}
ControlFlow::Continue(())
}
fn pre_visit_table_factor(&mut self, tf: &TableFactor) -> ControlFlow<()> {
match tf {
TableFactor::Table { .. }
| TableFactor::Derived { .. }
| TableFactor::NestedJoin { .. } => ControlFlow::Continue(()),
_ => ControlFlow::Break(()),
}
}
}
q.visit(&mut Deps { out }).is_continue()
}
fn table_factor(tf: &sqlparser::ast::TableFactor, out: &mut HashSet<String>) -> bool {
match tf {
sqlparser::ast::TableFactor::Table { name, .. } => {
if let Some(last) = name.0.last() {
out.insert(last.value.clone());
}
true
}
_ => false,
}
}
pub(crate) fn extract_tables(sql: &str) -> Option<HashSet<String>> {
use sqlparser::ast::Statement;
use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser;
let stmts = Parser::parse_sql(&SQLiteDialect {}, sql).ok()?;
let mut out = HashSet::new();
for stmt in &stmts {
let ok = match stmt {
Statement::Query(q) => query_tables(q, &mut out),
Statement::Insert(ins) => {
out.insert(ins.table_name.0.last()?.value.clone());
true
}
Statement::Update { table, .. } => table_factor(&table.relation, &mut out),
Statement::Delete(del) => {
let from = match &del.from {
sqlparser::ast::FromTable::WithFromKeyword(v)
| sqlparser::ast::FromTable::WithoutKeyword(v) => v,
};
from.iter().all(|t| table_factor(&t.relation, &mut out))
}
Statement::Pragma { .. } => true, _ => false,
};
if !ok {
return None;
}
}
Some(out)
}
pub(crate) enum WriteTables {
ReadOnly,
Tables(HashSet<String>),
Unknown,
}
fn obvious_single_read(sql: &str) -> bool {
let mut sql = sql.trim();
loop {
if let Some(comment) = sql.strip_prefix("--") {
let Some(end) = comment.find('\n') else {
return false;
};
sql = comment[end + 1..].trim_start();
} else if let Some(comment) = sql.strip_prefix("/*") {
let Some(end) = comment.find("*/") else {
return false;
};
sql = comment[end + 2..].trim_start();
} else {
break;
}
}
let sql = sql.strip_suffix(';').unwrap_or(sql).trim_end();
if sql.contains(';') {
return false;
}
let Some(keyword) = sql.split_ascii_whitespace().next() else {
return false;
};
keyword.eq_ignore_ascii_case("SELECT") || keyword.eq_ignore_ascii_case("EXPLAIN")
}
pub(crate) fn extract_write_tables(sql: &str) -> WriteTables {
use sqlparser::ast::Statement;
use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser;
let Ok(stmts) = Parser::parse_sql(&SQLiteDialect {}, sql) else {
return if obvious_single_read(sql) {
WriteTables::ReadOnly
} else {
WriteTables::Unknown
};
};
let mut out = HashSet::new();
let mut any_write = false;
for stmt in &stmts {
match stmt {
Statement::Query(q) => match q.body.as_ref() {
sqlparser::ast::SetExpr::Insert(_)
| sqlparser::ast::SetExpr::Update(_)
| sqlparser::ast::SetExpr::Table(_) => return WriteTables::Unknown,
_ => {}
},
Statement::Explain { .. } => {}
Statement::Pragma { .. } => return WriteTables::Unknown,
Statement::Insert(ins) => {
any_write = true;
match ins.table_name.0.last() {
Some(last) => {
out.insert(last.value.clone());
}
None => return WriteTables::Unknown,
}
}
Statement::Update { table, .. } => {
any_write = true;
if !table_factor(&table.relation, &mut out) {
return WriteTables::Unknown;
}
}
Statement::Delete(del) => {
any_write = true;
let from = match &del.from {
sqlparser::ast::FromTable::WithFromKeyword(v)
| sqlparser::ast::FromTable::WithoutKeyword(v) => v,
};
if !from.iter().all(|t| table_factor(&t.relation, &mut out)) {
return WriteTables::Unknown;
}
}
_ => return WriteTables::Unknown,
}
}
if any_write {
WriteTables::Tables(out)
} else {
WriteTables::ReadOnly
}
}
#[derive(Clone, Default)]
pub(crate) enum OwnedParams {
#[default]
None,
Positional(Vec<Value>),
Named(Vec<(String, Value)>),
}
impl OwnedParams {
pub(crate) fn from_dyn(params: &[&dyn ToSql]) -> Result<Self> {
if params.is_empty() {
return Ok(Self::None);
}
let vals: Result<Vec<Value>> = params
.iter()
.map(|p| crate::entity::to_owned_value(*p))
.collect();
Ok(Self::Positional(vals?))
}
fn bind(&self, stmt: &mut rusqlite::Statement<'_>) -> Result<()> {
match self {
Self::None => {}
Self::Positional(vals) => {
for (i, v) in vals.iter().enumerate() {
stmt.raw_bind_parameter(i + 1, v)?;
}
}
Self::Named(pairs) => {
for (k, v) in pairs {
let idx = stmt
.parameter_index(k)?
.ok_or_else(|| Error::Config(format!("알 수 없는 파라미터: {k}")))?;
stmt.raw_bind_parameter(idx, v)?;
}
}
}
Ok(())
}
}
pub(crate) fn query_all_owned<T: FromRow>(
conn: &Connection,
sql: &str,
params: &OwnedParams,
) -> Result<Vec<T>> {
let mut stmt = conn.prepare(sql)?;
params.bind(&mut stmt)?;
let mut rows = stmt.raw_query();
let mut out = Vec::new();
while let Some(row) = rows.next()? {
out.push(T::from_row(row)?);
}
Ok(out)
}
pub(crate) fn query_optional_owned<T: FromRow>(
conn: &Connection,
sql: &str,
params: &OwnedParams,
) -> Result<Option<T>> {
let mut stmt = conn.prepare(sql)?;
params.bind(&mut stmt)?;
let mut rows = stmt.raw_query();
match rows.next()? {
Some(row) => Ok(Some(T::from_row(row)?)),
None => Ok(None),
}
}
pub(crate) fn query_scalar_owned<T: rusqlite::types::FromSql>(
conn: &Connection,
sql: &str,
params: &OwnedParams,
) -> Result<T> {
let mut stmt = conn.prepare(sql)?;
params.bind(&mut stmt)?;
let mut rows = stmt.raw_query();
match rows.next()? {
Some(row) => Ok(row.get(0)?),
None => Err(Error::NotFound),
}
}
pub(crate) enum Msg {
Initialize(
crate::database::ConnCallback,
bool,
std::sync::mpsc::SyncSender<Result<()>>,
),
Invalidate(Option<HashSet<String>>),
Refresh(u64),
RefreshNew(u64),
Shutdown,
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum RefreshKind {
Full,
NewOnly,
}
type RefreshFn = Arc<dyn Fn(&Connection, RefreshKind) + Send + Sync>;
struct SubEntry {
tables: Option<HashSet<String>>,
refresh: RefreshFn,
close: Box<dyn Fn() + Send + Sync>,
}
pub(crate) struct Tracker {
subs: Mutex<HashMap<u64, SubEntry>>,
next_id: AtomicU64,
tx: Sender<Msg>,
notifier_thread: Arc<std::sync::OnceLock<std::thread::ThreadId>>,
}
impl Tracker {
pub(crate) fn start(
notifier_conn: Connection,
) -> Result<(Arc<Tracker>, std::thread::JoinHandle<()>)> {
notifier_conn
.pragma_update(None, "query_only", "ON")
.map_err(Error::from)?;
let (tx, rx) = channel::<Msg>();
let tracker = Arc::new(Tracker {
subs: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
tx,
notifier_thread: Arc::new(std::sync::OnceLock::new()),
});
let t2 = Arc::clone(&tracker);
let handle = std::thread::Builder::new()
.name("roomrs-notifier".into())
.spawn(move || t2.notifier_loop(rx, notifier_conn))
.map_err(|e| Error::Internal(format!("노티파이어 스레드 생성 실패: {e}")))?;
Ok((tracker, handle))
}
fn notifier_loop(&self, rx: Receiver<Msg>, conn: Connection) {
let _ = self.notifier_thread.set(std::thread::current().id());
loop {
let first = match rx.recv() {
Ok(m) => m,
Err(_) => return, };
if let Msg::Initialize(cb, read_uncommitted, reply) = first {
let callback_result = conn
.pragma_update(None, "query_only", "OFF")
.map_err(Error::from)
.and_then(|()| {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cb(&conn)))
.map_err(|_| Error::Internal("on_open 콜백 panic".into()))
.and_then(|result| result)
});
let rollback_result = if !conn.is_autocommit() {
conn.execute_batch("ROLLBACK").map_err(Error::from)
} else {
Ok(())
};
let read_uncommitted_result = if read_uncommitted {
conn.pragma_update(None, "read_uncommitted", "ON")
.map_err(Error::from)
} else {
Ok(())
};
let restore_result = conn
.pragma_update(None, "query_only", "ON")
.map_err(Error::from);
let result = callback_result
.and(rollback_result)
.and(read_uncommitted_result)
.and(restore_result);
let _ = reply.send(result);
continue;
}
let mut all = false;
let mut tables: HashSet<String> = HashSet::new();
let mut refresh_ids: HashSet<u64> = HashSet::new();
let mut new_ids: HashSet<u64> = HashSet::new();
let mut msg = Some(first);
loop {
match msg.take() {
Some(Msg::Shutdown) => return,
Some(Msg::Initialize(_, _, reply)) => {
let _ = reply.send(Err(Error::Internal(
"노티파이어 초기화 메시지 순서 오류".into(),
)));
}
Some(Msg::Invalidate(None)) => all = true,
Some(Msg::Invalidate(Some(ts))) => tables.extend(ts),
Some(Msg::Refresh(id)) => {
refresh_ids.insert(id);
}
Some(Msg::RefreshNew(id)) => {
new_ids.insert(id);
}
None => {}
}
match rx.try_recv() {
Ok(m) => msg = Some(m),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => break,
}
}
log::trace!(
"notifier debounce merged: all={all}, tables={tables:?}, refresh={}, new={}",
refresh_ids.len(),
new_ids.len()
);
let targets: Vec<(RefreshFn, RefreshKind)> = {
let subs = plock(&self.subs);
subs.iter()
.filter_map(|(id, e)| {
let full = refresh_ids.contains(id)
|| (e.tables.is_some()
&& (all
|| e.tables
.as_ref()
.expect("직전 검사")
.iter()
.any(|t| tables.contains(t))));
if full {
Some((Arc::clone(&e.refresh), RefreshKind::Full))
} else if new_ids.contains(id) {
Some((Arc::clone(&e.refresh), RefreshKind::NewOnly))
} else {
None
}
})
.collect()
};
for (refresh, kind) in targets {
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| refresh(&conn, kind)))
.is_err()
{
log::warn!("live query refresh panicked — isolated, notifier continues");
}
}
}
}
pub(crate) fn initialize(
&self,
cb: crate::database::ConnCallback,
read_uncommitted: bool,
) -> Result<()> {
let (reply_tx, reply_rx) = std::sync::mpsc::sync_channel(1);
self.tx
.send(Msg::Initialize(cb, read_uncommitted, reply_tx))
.map_err(|_| Error::Closed)?;
reply_rx.recv().map_err(|_| Error::Closed)?
}
pub(crate) fn register(
&self,
tables: Option<HashSet<String>>,
refresh: RefreshFn,
close: Box<dyn Fn() + Send + Sync>,
) -> u64 {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
plock(&self.subs).insert(
id,
SubEntry {
tables,
refresh,
close,
},
);
id
}
pub(crate) fn unregister(&self, id: u64) {
plock(&self.subs).remove(&id);
}
pub(crate) fn set_tables(&self, id: u64, tables: HashSet<String>) {
if let Some(e) = plock(&self.subs).get_mut(&id) {
e.tables = Some(tables);
}
}
pub(crate) fn invalidate(&self, tables: Option<HashSet<String>>) {
log::debug!("invalidation emitted: tables={tables:?}");
let _ = self.tx.send(Msg::Invalidate(tables));
}
pub(crate) fn request_refresh(&self, id: u64) {
let _ = self.tx.send(Msg::Refresh(id));
}
pub(crate) fn request_refresh_new(&self, id: u64) {
let _ = self.tx.send(Msg::RefreshNew(id));
}
pub(crate) fn shutdown(&self) {
let entries: Vec<SubEntry> = plock(&self.subs).drain().map(|(_, e)| e).collect();
for e in &entries {
(e.close)();
}
let _ = self.tx.send(Msg::Shutdown);
}
}
type CallbackList<T> = Vec<(u64, bool, Box<dyn FnMut(T) + Send>)>;
struct DeliveryState {
closed: bool,
active: usize,
}
struct SubShared<T> {
value_slot: Mutex<Option<Result<T>>>,
value_cv: Condvar,
#[cfg(feature = "stream")]
stream_waker: Mutex<Option<std::task::Waker>>,
callbacks: Mutex<CallbackList<T>>,
delivery: Mutex<DeliveryState>,
delivery_cv: Condvar,
next_cb_id: AtomicU64,
deferred_remove: Mutex<Vec<u64>>,
epoch: AtomicU64,
unknown_deps: AtomicBool,
closed: AtomicBool,
last_value: Mutex<Option<T>>,
}
impl<T: Clone> SubShared<T> {
fn publish(&self, value: Result<T>) {
let mut slot = plock(&self.value_slot);
if self.closed.load(Ordering::Acquire) {
return;
}
*slot = Some(value);
drop(slot);
self.value_cv.notify_all();
#[cfg(feature = "stream")]
if let Some(waker) = plock(&self.stream_waker).take() {
waker.wake();
}
}
fn close_terminal(&self, wait_callbacks: bool) {
{
let mut delivery = plock(&self.delivery);
delivery.closed = true;
}
let mut slot = plock(&self.value_slot);
self.closed.store(true, Ordering::Release);
*slot = Some(Err(Error::Closed));
drop(slot);
self.value_cv.notify_all();
#[cfg(feature = "stream")]
if let Some(waker) = plock(&self.stream_waker).take() {
waker.wake();
}
if wait_callbacks {
let mut delivery = plock(&self.delivery);
while delivery.active != 0 {
delivery = self
.delivery_cv
.wait(delivery)
.unwrap_or_else(|e| e.into_inner());
}
}
}
fn deliver(&self, v: &T, fresh_only: bool) {
{
let mut delivery = plock(&self.delivery);
if delivery.closed {
return;
}
delivery.active += 1;
}
let mut cbs: CallbackList<T> = {
let mut g = plock(&self.callbacks);
let deferred = std::mem::take(&mut *plock(&self.deferred_remove));
g.retain(|(id, _, _)| !deferred.contains(id));
std::mem::take(&mut *g)
};
for (id, fresh, cb) in cbs.iter_mut() {
if fresh_only && !*fresh {
continue;
}
if plock(&self.deferred_remove).contains(id) {
continue;
}
*fresh = false;
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cb(v.clone()))).is_err() {
log::warn!("live query callback panicked — isolated, other callbacks continue");
}
}
let mut g = plock(&self.callbacks);
let newly = std::mem::take(&mut *g);
cbs.extend(newly);
let deferred = std::mem::take(&mut *plock(&self.deferred_remove));
cbs.retain(|(id, _, _)| !deferred.contains(id));
*g = cbs;
let mut delivery = plock(&self.delivery);
delivery.active -= 1;
if delivery.active == 0 {
self.delivery_cv.notify_all();
}
}
}
fn unknown_deps_err() -> Error {
Error::UnknownDependencies(
"쿼리의 의존 테이블을 추출하지 못했습니다 — .watching(&[…]) 필요".into(),
)
}
fn is_table_locked<T>(out: &Result<T>) -> bool {
matches!(
out,
Err(Error::Sqlite(rusqlite::Error::SqliteFailure(fe, _)))
if fe.code == rusqlite::ErrorCode::DatabaseLocked
)
}
pub struct LiveQuery<T> {
id: u64,
tracker: Arc<Tracker>,
shared: Arc<SubShared<T>>,
params: Arc<Mutex<OwnedParams>>,
}
impl<T: Clone + Send + 'static> LiveQuery<T> {
pub(crate) fn new(
tracker: Arc<Tracker>,
sql: String,
params: OwnedParams,
tables: Option<HashSet<String>>,
run: impl Fn(&Connection, &str, &OwnedParams) -> Result<T> + Send + Sync + 'static,
) -> Self {
let params = Arc::new(Mutex::new(params));
let unknown = tables.is_none();
let validate_names = tables.clone();
let validate_pending = Arc::new(AtomicBool::new(validate_names.is_some()));
let shared = Arc::new(SubShared {
value_slot: Mutex::new(None),
value_cv: Condvar::new(),
#[cfg(feature = "stream")]
stream_waker: Mutex::new(None),
callbacks: Mutex::new(Vec::new()),
delivery: Mutex::new(DeliveryState {
closed: false,
active: 0,
}),
delivery_cv: Condvar::new(),
next_cb_id: AtomicU64::new(1),
deferred_remove: Mutex::new(Vec::new()),
epoch: AtomicU64::new(0),
unknown_deps: AtomicBool::new(unknown),
closed: AtomicBool::new(false),
last_value: Mutex::new(None),
});
let refresh: RefreshFn = {
let shared = Arc::clone(&shared);
let params = Arc::clone(¶ms);
let validate_pending = Arc::clone(&validate_pending);
Arc::new(move |conn: &Connection, kind: RefreshKind| {
if kind == RefreshKind::NewOnly {
let cached = plock(&shared.last_value).clone();
if let Some(v) = cached {
shared.deliver(&v, true);
return;
}
}
if validate_pending.swap(false, Ordering::AcqRel) {
if let Some(names) = &validate_names {
let all_tables = names.iter().all(|name| {
conn.query_row(
"SELECT count(*) FROM sqlite_master \
WHERE type='table' AND name=?1 COLLATE NOCASE",
[name],
|row| row.get::<_, i64>(0),
) == Ok(1)
});
if !all_tables {
shared.publish(Err(unknown_deps_err()));
return;
}
}
}
let epoch = shared.epoch.load(Ordering::Acquire);
let p = plock(¶ms).clone();
let mut out = run(conn, &sql, &p);
if is_table_locked(&out) {
log::warn!("SQLITE_LOCKED during live refresh — retrying once");
std::thread::sleep(Duration::from_millis(10));
out = run(conn, &sql, &p);
}
if shared.epoch.load(Ordering::Acquire) != epoch {
return;
}
match out {
Ok(v) => {
*plock(&shared.last_value) = Some(v.clone());
shared.deliver(&v, false);
shared.publish(Ok(v));
}
Err(e) => {
log::error!("live query refresh failed: {e}");
shared.publish(Err(e));
}
}
})
};
let close: Box<dyn Fn() + Send + Sync> = {
let shared = Arc::clone(&shared);
let notifier_thread = Arc::clone(&tracker.notifier_thread);
Box::new(move || {
let on_notifier = notifier_thread.get() == Some(&std::thread::current().id());
shared.close_terminal(!on_notifier);
})
};
let id = tracker.register(tables, refresh, close);
let lq = LiveQuery {
id,
tracker,
shared,
params,
};
if !unknown {
lq.tracker.request_refresh(lq.id);
}
lq
}
fn take_unknown_deps(&self) -> bool {
self.shared.unknown_deps.swap(false, Ordering::AcqRel)
}
pub fn recv(&self) -> Result<T> {
if self.take_unknown_deps() {
return Err(unknown_deps_err());
}
let mut slot = plock(&self.shared.value_slot);
loop {
if let Some(value) = slot.take() {
return value;
}
if self.shared.closed.load(Ordering::Acquire) {
return Err(Error::Closed);
}
slot = self
.shared
.value_cv
.wait(slot)
.unwrap_or_else(|e| e.into_inner());
}
}
pub fn recv_timeout(&self, d: Duration) -> Result<Option<T>> {
if self.take_unknown_deps() {
return Err(unknown_deps_err());
}
let mut slot = plock(&self.shared.value_slot);
if let Some(value) = slot.take() {
return value.map(Some);
}
if self.shared.closed.load(Ordering::Acquire) {
return Err(Error::Closed);
}
let (mut slot, _) = self
.shared
.value_cv
.wait_timeout_while(slot, d, |slot| {
slot.is_none() && !self.shared.closed.load(Ordering::Acquire)
})
.unwrap_or_else(|e| e.into_inner());
if let Some(value) = slot.take() {
value.map(Some)
} else if self.shared.closed.load(Ordering::Acquire) {
Err(Error::Closed)
} else {
Ok(None)
}
}
pub fn try_recv(&self) -> Result<Option<T>> {
if self.take_unknown_deps() {
return Err(unknown_deps_err());
}
if let Some(value) = plock(&self.shared.value_slot).take() {
value.map(Some)
} else if self.shared.closed.load(Ordering::Acquire) {
Err(Error::Closed)
} else {
Ok(None)
}
}
pub fn iter(&self) -> impl Iterator<Item = Result<T>> + '_ {
let mut closed = false;
std::iter::from_fn(move || {
if closed {
return None;
}
let v = self.recv();
if matches!(v, Err(Error::Closed)) {
closed = true;
}
Some(v)
})
}
#[must_use = "가드를 버리면 구독이 즉시 해지됩니다 (명세 C-7)"]
pub fn subscribe(&self, f: impl FnMut(T) + Send + 'static) -> SubscriptionGuard<T> {
let cb_id = self.shared.next_cb_id.fetch_add(1, Ordering::Relaxed);
plock(&self.shared.callbacks).push((cb_id, true, Box::new(f)));
self.tracker.request_refresh_new(self.id);
SubscriptionGuard {
shared: Arc::clone(&self.shared),
cb_id,
detached: false,
}
}
pub fn rebind(&self, params: &[&dyn ToSql]) -> Result<()> {
let owned = OwnedParams::from_dyn(params)?;
*plock(&self.params) = owned;
self.shared.epoch.fetch_add(1, Ordering::AcqRel);
*plock(&self.shared.last_value) = None;
self.tracker.request_refresh(self.id);
Ok(())
}
pub fn watching(self, tables: &[&str]) -> Self {
let set: HashSet<String> = tables.iter().map(|s| s.to_string()).collect();
self.tracker.set_tables(self.id, set);
self.shared.unknown_deps.store(false, Ordering::Release);
self.tracker.request_refresh(self.id);
self
}
}
impl<T> Drop for LiveQuery<T> {
fn drop(&mut self) {
self.tracker.unregister(self.id);
}
}
#[cfg(feature = "stream")]
impl<T: Clone + Send + 'static> LiveQuery<T> {
pub fn into_stream(self) -> impl futures_core::Stream<Item = Result<T>> + Send {
LiveStream { query: self }
}
}
#[cfg(feature = "stream")]
struct LiveStream<T> {
query: LiveQuery<T>,
}
#[cfg(feature = "stream")]
impl<T: Clone + Send + 'static> futures_core::Stream for LiveStream<T> {
type Item = Result<T>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let this = self.get_mut();
match this.query.try_recv() {
Ok(Some(value)) => std::task::Poll::Ready(Some(Ok(value))),
Err(Error::Closed) => std::task::Poll::Ready(None),
Err(error) => std::task::Poll::Ready(Some(Err(error))),
Ok(None) => {
*plock(&this.query.shared.stream_waker) = Some(cx.waker().clone());
match this.query.try_recv() {
Ok(Some(value)) => std::task::Poll::Ready(Some(Ok(value))),
Err(Error::Closed) => std::task::Poll::Ready(None),
Err(error) => std::task::Poll::Ready(Some(Err(error))),
Ok(None) => std::task::Poll::Pending,
}
}
}
}
}
pub struct SubscriptionGuard<T> {
shared: Arc<SubShared<T>>,
cb_id: u64,
detached: bool,
}
impl<T> SubscriptionGuard<T> {
pub fn detach(mut self) {
self.detached = true;
}
}
impl<T> Drop for SubscriptionGuard<T> {
fn drop(&mut self) {
if self.detached {
return;
}
plock(&self.shared.callbacks).retain(|(id, _, _)| *id != self.cb_id);
plock(&self.shared.deferred_remove).push(self.cb_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn close_is_terminal_against_late_publish() {
let shared = SubShared {
value_slot: Mutex::new(None),
value_cv: Condvar::new(),
#[cfg(feature = "stream")]
stream_waker: Mutex::new(None),
callbacks: Mutex::new(Vec::new()),
delivery: Mutex::new(DeliveryState {
closed: false,
active: 0,
}),
delivery_cv: Condvar::new(),
next_cb_id: AtomicU64::new(1),
deferred_remove: Mutex::new(Vec::new()),
epoch: AtomicU64::new(0),
unknown_deps: AtomicBool::new(false),
closed: AtomicBool::new(false),
last_value: Mutex::new(None::<i64>),
};
shared.close_terminal(true);
shared.publish(Ok(99));
assert!(matches!(
plock(&shared.value_slot).take(),
Some(Err(Error::Closed))
));
}
#[test]
fn close_waits_for_in_flight_callback() {
let shared = Arc::new(SubShared {
value_slot: Mutex::new(None),
value_cv: Condvar::new(),
#[cfg(feature = "stream")]
stream_waker: Mutex::new(None),
callbacks: Mutex::new(Vec::new()),
delivery: Mutex::new(DeliveryState {
closed: false,
active: 0,
}),
delivery_cv: Condvar::new(),
next_cb_id: AtomicU64::new(1),
deferred_remove: Mutex::new(Vec::new()),
epoch: AtomicU64::new(0),
unknown_deps: AtomicBool::new(false),
closed: AtomicBool::new(false),
last_value: Mutex::new(None::<i64>),
});
let (entered_tx, entered_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
plock(&shared.callbacks).push((
1,
false,
Box::new(move |_| {
entered_tx.send(()).unwrap();
release_rx.recv().unwrap();
}),
));
std::thread::scope(|scope| {
let delivering = Arc::clone(&shared);
scope.spawn(move || delivering.deliver(&1, false));
entered_rx.recv().unwrap();
let closing = Arc::clone(&shared);
let (closed_tx, closed_rx) = std::sync::mpsc::channel();
scope.spawn(move || {
closing.close_terminal(true);
closed_tx.send(()).unwrap();
});
assert!(closed_rx.recv_timeout(Duration::from_millis(20)).is_err());
release_tx.send(()).unwrap();
closed_rx.recv_timeout(Duration::from_secs(1)).unwrap();
});
shared.deliver(&2, false);
assert_eq!(plock(&shared.delivery).active, 0);
}
#[test]
fn cte_write_classified_unknown() {
let w = extract_write_tables("WITH x AS (SELECT 1 AS a) INSERT INTO t (a) SELECT a FROM x");
assert!(matches!(w, WriteTables::Unknown), "CTE-INSERT = Unknown");
let u = extract_write_tables("WITH x AS (SELECT 1 AS a) UPDATE t SET a = 1");
assert!(matches!(u, WriteTables::Unknown), "CTE-UPDATE = Unknown");
let r = extract_write_tables("WITH x AS (SELECT 1 AS a) SELECT a FROM x");
assert!(matches!(r, WriteTables::ReadOnly), "읽기 CTE = ReadOnly");
}
#[test]
fn sqlite_select_parse_failure_is_read_only() {
let result = extract_write_tables("SELECT * FROM t INDEXED BY idx_t");
assert!(matches!(result, WriteTables::ReadOnly));
let commented = extract_write_tables("-- 조회\nSELECT * FROM t INDEXED BY idx_t");
assert!(matches!(commented, WriteTables::ReadOnly));
}
#[test]
fn sqlite_explain_parse_failure_is_read_only() {
let result = extract_write_tables("EXPLAIN SELECT * FROM t INDEXED BY idx_t");
assert!(matches!(result, WriteTables::ReadOnly));
let write = extract_write_tables("EXPLAIN INSERT OR CUSTOM INTO t VALUES (1)");
assert!(matches!(write, WriteTables::ReadOnly));
}
#[test]
fn malformed_write_stays_unknown() {
let result = extract_write_tables("INSERT OR CUSTOM INTO t VALUES (1)");
assert!(matches!(result, WriteTables::Unknown));
}
#[test]
fn pragma_stays_unknown() {
assert!(matches!(
extract_write_tables("PRAGMA user_version = 2"),
WriteTables::Unknown
));
assert!(matches!(
extract_write_tables("PRAGMA user_version"),
WriteTables::Unknown
));
}
#[test]
fn explain_write_is_read_only() {
assert!(matches!(
extract_write_tables("EXPLAIN INSERT INTO t VALUES (1)"),
WriteTables::ReadOnly
));
}
#[test]
fn multi_statement_with_write_collects_table() {
let WriteTables::Tables(tables) =
extract_write_tables("SELECT 1; INSERT INTO t VALUES (1)")
else {
panic!("SELECT + INSERT는 Tables여야 함");
};
assert_eq!(tables, HashSet::from(["t".to_owned()]));
}
}