use super::Session;
use crate::api_core::error::{CoreError, CoreResult};
use crate::api_core::types::{
ExecutionMetadata, QueryRequestBuilder, QueryResult as CoreQueryResult, SpaceConfig,
};
use crate::embedded::result::QueryResult;
use linkrs_core::types::DataSet;
use linkrs_core::Edge;
use linkrs_core::Value;
use linkrs_core::Vertex;
use linkrs_query::classify_dml;
use linkrs_query::executor::base::ExecutionResult;
use linkrs_query::executor::expression::functions::FunctionRegistry;
use linkrs_query::DmlOp;
use linkrs_storage::StorageClient;
use parking_lot::RwLock;
use parking_lot::RwLockWriteGuard;
use std::collections::HashMap;
use std::sync::Arc;
impl<S: StorageClient + Clone + Send + Sync + 'static + linkrs_storage::UndoTarget> Session<S> {
pub fn function_registry(&self) -> Arc<RwLock<FunctionRegistry>> {
Arc::clone(&self.function_registry)
}
pub fn changes(&self) -> u64 {
self.statistics.last_changes()
}
pub fn total_changes(&self) -> u64 {
self.statistics.total_changes()
}
pub fn last_insert_vertex_id(&self) -> Option<u64> {
self.statistics.last_insert_vertex_id()
}
pub fn last_insert_edge_id(&self) -> Option<u64> {
self.statistics.last_insert_edge_id()
}
pub fn statistics(&self) -> &linkrs_core::SessionStatistics {
&self.statistics
}
pub fn use_space(&mut self, space_name: &str) -> CoreResult<()> {
let space_id = self.db.schema_api.use_space(space_name)?;
*self.space_id.write() = Some(space_id);
*self.space_name.write() = Some(space_name.to_string());
Ok(())
}
pub fn current_space(&self) -> Option<String> {
self.space_name.read().clone()
}
pub fn current_space_id(&self) -> Option<u64> {
*self.space_id.read()
}
pub(crate) fn notify_dml(&self, query: &str, rows: u64) -> Option<DmlOp> {
let op = classify_dml(query)?;
let space = self
.current_space()
.unwrap_or_else(|| "default".to_string());
self.notify_dml_op(op, &space, rows);
Some(op)
}
pub(crate) fn notify_dml_op(&self, op: DmlOp, space: &str, rows: u64) {
self.db.hooks.emit_dml(op, space, rows);
}
#[cfg(feature = "c_api")]
pub(crate) fn has_dml_observers(&self) -> bool {
self.db.hooks.has_dml_observers()
}
pub(crate) fn update_space_from_result(&self, result: &CoreQueryResult) {
let columns = result.columns();
if !columns.iter().any(|c| c == "space_name") {
return;
}
let row = match result.rows().first() {
Some(r) => r,
None => return,
};
let name = columns
.iter()
.position(|c| c == "space_name")
.and_then(|idx| row.get(idx))
.and_then(|v| match v {
Value::String(s) => Some(s.to_string()),
_ => None,
});
let name = match name {
Some(n) => n,
None => return,
};
let id = columns
.iter()
.position(|c| c == "space_id")
.and_then(|idx| row.get(idx))
.and_then(|v| match v {
Value::BigInt(i) => Some(*i as u64),
_ => None,
});
let id = match id {
Some(i) => i,
None => return,
};
*self.space_id.write() = Some(id);
*self.space_name.write() = Some(name);
}
pub fn set_auto_commit(&mut self, auto_commit: bool) {
self.auto_commit = auto_commit;
}
pub fn auto_commit(&self) -> bool {
self.auto_commit
}
pub fn interrupt(&self) {
self.interrupted
.store(true, std::sync::atomic::Ordering::SeqCst);
}
pub fn is_interrupted(&self) -> bool {
self.interrupted.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn clear_interrupt(&self) {
self.interrupted
.store(false, std::sync::atomic::Ordering::SeqCst);
}
fn ensure_not_interrupted(&self) -> CoreResult<()> {
if self.is_interrupted() {
return Err(CoreError::QueryExecutionFailed(
"query interrupted".to_string(),
));
}
Ok(())
}
pub(crate) fn ensure_writable_op(&self) -> CoreResult<()> {
if self.db.read_only {
return Err(CoreError::StorageError(
"database is read-only; write operations are not allowed".to_string(),
));
}
Ok(())
}
pub(crate) fn ensure_writable(&self, query: &str) -> CoreResult<()> {
if !self.db.read_only {
return Ok(());
}
if classify_dml(query).is_some() {
return self.ensure_writable_op();
}
let keyword = leading_keyword(query);
if keyword.eq_ignore_ascii_case("LOAD")
|| keyword.eq_ignore_ascii_case("IMPORT")
|| keyword.eq_ignore_ascii_case("EXPORT")
|| keyword.eq_ignore_ascii_case("ATTACH")
|| keyword.eq_ignore_ascii_case("COPY")
|| keyword.eq_ignore_ascii_case("INSTALL")
|| keyword.eq_ignore_ascii_case("UNINSTALL")
|| keyword.eq_ignore_ascii_case("UPDATE")
{
return self.ensure_writable_op();
}
Ok(())
}
pub fn set_variable(&self, name: String, value: Value) {
self.session_variables
.set_variable(name, value, self.current_transaction.read().is_some());
}
pub fn variables_snapshot(&self) -> HashMap<String, Value> {
self.session_variables.variables_snapshot()
}
pub(crate) fn session_query_request(&self) -> QueryRequestBuilder {
QueryRequestBuilder::new()
.space_id(*self.space_id.read())
.space_name(self.space_name.read().clone())
.auto_commit(self.auto_commit)
.session_variables(Some(self.variables_snapshot()))
}
pub(crate) fn reject_server_only_config_intent(result: &CoreQueryResult) -> CoreResult<()> {
use linkrs_query::executor::base::ExecutionResult as BaseExecutionResult;
match &result.execution {
BaseExecutionResult::ShowConfigs { .. }
| BaseExecutionResult::ConfigUpdate { .. } => {
Err(CoreError::InvalidParameter(
"SHOW CONFIGS and UPDATE CONFIGS require the server unary query interface and are not supported in embedded sessions"
.to_string(),
))
}
_ => Ok(()),
}
}
pub(crate) fn single_message_result(message: String) -> QueryResult {
let columns = vec!["result".to_string()];
let rows = vec![vec![Value::string(message)]];
let execution = ExecutionResult::from_data_set(DataSet::from_rows(rows, columns));
QueryResult::from_core(CoreQueryResult::new(
execution,
ExecutionMetadata {
rows_returned: 1,
..Default::default()
},
))
}
#[cfg(feature = "c_api")]
pub(crate) fn space_id(&self) -> Option<u64> {
*self.space_id.read()
}
pub(crate) fn txn_manager(&self) -> Arc<linkrs_transaction::TransactionManager> {
self.db.txn_manager.clone()
}
pub(crate) fn storage_mut(&self) -> RwLockWriteGuard<'_, S> {
self.db.storage.write()
}
pub(crate) fn space_name(&self) -> Option<String> {
self.space_name.read().clone()
}
pub(crate) fn current_transaction_id(&self) -> Option<linkrs_transaction::TransactionId> {
*self.current_transaction.read()
}
pub(crate) fn query_api_mut(&self) -> RwLockWriteGuard<'_, QueryApi<S>> {
self.db.query_api.as_ref().write()
}
pub fn batch_inserter(
&self,
batch_size: usize,
) -> crate::embedded::batch::BatchInserter<'_, S> {
crate::embedded::batch::BatchInserter::new(self, batch_size)
}
pub fn batch_insert_vertices(&self, vertices: Vec<Vertex>) -> CoreResult<usize> {
self.ensure_writable_op()?;
let space_name = self
.space_name()
.ok_or_else(|| CoreError::InvalidParameter("No graph space selected".to_string()))?;
let count = vertices.len();
let mut storage = self.storage_mut();
storage
.batch_insert_vertices(&space_name, vertices)
.map_err(|e| CoreError::StorageError(e.to_string()))?;
drop(storage);
self.notify_dml_op(DmlOp::Insert, &space_name, count as u64);
Ok(count)
}
pub fn batch_insert_edges(&self, edges: Vec<Edge>) -> CoreResult<usize> {
self.ensure_writable_op()?;
let space_name = self
.space_name()
.ok_or_else(|| CoreError::InvalidParameter("No graph space selected".to_string()))?;
let count = edges.len();
let mut storage = self.storage_mut();
storage
.batch_insert_edges(&space_name, edges)
.map_err(|e| CoreError::StorageError(e.to_string()))?;
drop(storage);
self.notify_dml_op(DmlOp::Insert, &space_name, count as u64);
Ok(count)
}
pub fn create_space(&self, name: &str, config: SpaceConfig) -> CoreResult<()> {
self.ensure_writable_op()?;
self.db.schema_api.create_space(name, config)
}
pub fn drop_space(&self, name: &str) -> CoreResult<()> {
self.ensure_writable_op()?;
self.db.schema_api.drop_space(name)
}
pub fn list_spaces(&self) -> CoreResult<Vec<String>> {
let storage = self.db.storage.write();
let spaces = storage
.list_spaces()
.map_err(|e| CoreError::StorageError(e.to_string()))?;
Ok(spaces.into_iter().map(|s| s.space_name).collect())
}
}
fn leading_keyword(query: &str) -> &str {
let rest = query.trim_start();
let end = rest
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.unwrap_or(rest.len());
&rest[..end]
}