Skip to main content

reifydb_sub_server/
subscribe.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::fmt;
5
6#[cfg(not(reifydb_single_threaded))]
7use reifydb_core::error::diagnostic::internal::internal;
8use reifydb_core::interface::catalog::id::SubscriptionId;
9use reifydb_engine::engine::StandardEngine;
10#[cfg(not(reifydb_single_threaded))]
11use reifydb_type::error::Error;
12#[cfg(not(reifydb_single_threaded))]
13use reifydb_type::value::frame::column::FrameColumn;
14use reifydb_type::{
15	Result as TypeResult,
16	params::Params,
17	value::{Value, frame::frame::Frame, identity::IdentityId},
18};
19#[cfg(not(reifydb_single_threaded))]
20use tracing::debug;
21#[allow(unused_imports)]
22use tracing::error;
23
24/// Error type for subscription creation.
25pub enum CreateSubscriptionError {
26	Execute(ExecuteError),
27	ExtractionFailed,
28}
29
30impl fmt::Display for CreateSubscriptionError {
31	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32		match self {
33			CreateSubscriptionError::Execute(e) => write!(f, "{}", e),
34			CreateSubscriptionError::ExtractionFailed => write!(f, "Failed to extract subscription ID"),
35		}
36	}
37}
38
39impl fmt::Debug for CreateSubscriptionError {
40	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41		match self {
42			CreateSubscriptionError::Execute(e) => f.debug_tuple("Execute").field(e).finish(),
43			CreateSubscriptionError::ExtractionFailed => write!(f, "ExtractionFailed"),
44		}
45	}
46}
47
48impl From<ExecuteError> for CreateSubscriptionError {
49	fn from(err: ExecuteError) -> Self {
50		CreateSubscriptionError::Execute(err)
51	}
52}
53
54/// Result of creating a subscription: either local or remote.
55pub enum CreateSubscriptionResult {
56	Local(SubscriptionId),
57	Remote {
58		address: String,
59		rql: String,
60		token: Option<String>,
61	},
62}
63
64#[cfg(not(reifydb_single_threaded))]
65use reifydb_core::actors::server::Operation;
66#[cfg(not(reifydb_single_threaded))]
67use tokio::task::spawn_blocking;
68
69use crate::execute::ExecuteError;
70#[cfg(not(reifydb_single_threaded))]
71use crate::{
72	dispatch::dispatch_subscribe,
73	interceptor::{RequestContext, RequestMetadata},
74	state::AppState,
75};
76
77/// Execute `CREATE SUBSCRIPTION AS { query }` and extract the subscription ID from the result.
78#[cfg(not(reifydb_single_threaded))]
79pub async fn create_subscription(
80	state: &AppState,
81	identity: IdentityId,
82	rql: &str,
83	metadata: RequestMetadata,
84) -> Result<CreateSubscriptionResult, CreateSubscriptionError> {
85	let subscription_rql = format!("CREATE SUBSCRIPTION AS {{ {} }}", rql);
86	debug!("Subscription rql: {}", subscription_rql);
87
88	let ctx = RequestContext {
89		identity,
90		operation: Operation::Subscribe,
91		rql: subscription_rql,
92		params: Params::None,
93		metadata,
94	};
95	let (frames, _metrics) = dispatch_subscribe(state, ctx).await?;
96	let frame = frames.first().ok_or(CreateSubscriptionError::ExtractionFailed)?;
97
98	if let Some(remote) = extract_remote_result(frame)? {
99		return Ok(remote);
100	}
101	extract_local_result(frame)
102}
103
104#[cfg(not(reifydb_single_threaded))]
105fn extract_remote_result(frame: &Frame) -> Result<Option<CreateSubscriptionResult>, CreateSubscriptionError> {
106	let Some(addr_col) = frame.columns.iter().find(|c| c.name == "remote_address") else {
107		return Ok(None);
108	};
109	let address = match first_utf8_value(addr_col) {
110		Some(s) => s,
111		None => return Err(CreateSubscriptionError::ExtractionFailed),
112	};
113	let rql = frame
114		.columns
115		.iter()
116		.find(|c| c.name == "remote_rql")
117		.and_then(first_utf8_value)
118		.ok_or(CreateSubscriptionError::ExtractionFailed)?;
119	let token = frame.columns.iter().find(|c| c.name == "remote_token").and_then(first_utf8_value);
120	Ok(Some(CreateSubscriptionResult::Remote {
121		address,
122		rql,
123		token,
124	}))
125}
126
127#[cfg(not(reifydb_single_threaded))]
128fn extract_local_result(frame: &Frame) -> Result<CreateSubscriptionResult, CreateSubscriptionError> {
129	frame.columns
130		.iter()
131		.find(|c| c.name == "subscription_id")
132		.and_then(|col| {
133			if col.data.is_empty() {
134				None
135			} else {
136				Some(col.data.get_value(0))
137			}
138		})
139		.and_then(|value| match value {
140			Value::Uint8(id) => Some(SubscriptionId(id)),
141			other => {
142				error!("subscription_id column has wrong type: {:?}", other);
143				None
144			}
145		})
146		.map(CreateSubscriptionResult::Local)
147		.ok_or(CreateSubscriptionError::ExtractionFailed)
148}
149
150#[cfg(not(reifydb_single_threaded))]
151#[inline]
152fn first_utf8_value(col: &FrameColumn) -> Option<String> {
153	if col.data.is_empty() {
154		return None;
155	}
156	match col.data.get_value(0) {
157		Value::Utf8(s) => Some(s),
158		_ => None,
159	}
160}
161
162/// Extract the subscription ID from frames returned by `engine.subscribe_as`.
163///
164/// The engine returns a single-row frame with a `subscription_id` column
165/// containing a `Value::Uint8(id)`.
166pub fn extract_subscription_id(frames: &[Frame]) -> Option<SubscriptionId> {
167	let frame = frames.first()?;
168	frame.columns
169		.iter()
170		.find(|c| c.name == "subscription_id")
171		.and_then(|col| {
172			if !col.data.is_empty() {
173				Some(col.data.get_value(0))
174			} else {
175				None
176			}
177		})
178		.and_then(|value| match value {
179			Value::Uint8(id) => Some(SubscriptionId(id)),
180			_ => None,
181		})
182}
183
184/// Synchronous cleanup: drop subscription via DDL.
185pub fn cleanup_subscription_sync(engine: &StandardEngine, subscription_id: SubscriptionId) -> TypeResult<()> {
186	let rql = format!("drop subscription if exists subscription_{};", subscription_id.0);
187	engine.admin_as(IdentityId::system(), &rql, Params::None).check()?;
188	Ok(())
189}
190
191/// Async cleanup via a blocking task.
192#[cfg(not(reifydb_single_threaded))]
193pub async fn cleanup_subscription(state: &AppState, subscription_id: SubscriptionId) -> TypeResult<()> {
194	let engine = state.engine_clone();
195
196	spawn_blocking(move || cleanup_subscription_sync(&engine, subscription_id))
197		.await
198		.map_err(|e| Error(Box::new(internal(format!("Blocking task error: {:?}", e)))))?
199}