surrealdb_sql/
model.rs

1use crate::ctx::Context;
2use crate::dbs::{Options, Transaction};
3use crate::doc::CursorDoc;
4use crate::err::Error;
5use crate::value::Value;
6use derive::Store;
7use revision::revisioned;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10
11#[cfg(feature = "ml")]
12use crate::iam::Action;
13#[cfg(feature = "ml")]
14use crate::Permission;
15#[cfg(feature = "ml")]
16use futures::future::try_join_all;
17#[cfg(feature = "ml")]
18use std::collections::HashMap;
19#[cfg(feature = "ml")]
20use surrealml_core::execution::compute::ModelComputation;
21#[cfg(feature = "ml")]
22use surrealml_core::storage::surml_file::SurMlFile;
23
24#[cfg(feature = "ml")]
25const ARGUMENTS: &str = "The model expects 1 argument. The argument can be either a number, an object, or an array of numbers.";
26
27#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
28#[revisioned(revision = 1)]
29pub struct Model {
30	pub name: String,
31	pub version: String,
32	pub args: Vec<Value>,
33}
34
35impl fmt::Display for Model {
36	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37		write!(f, "ml::{}<{}>(", self.name, self.version)?;
38		for (idx, p) in self.args.iter().enumerate() {
39			if idx != 0 {
40				write!(f, ",")?;
41			}
42			write!(f, "{}", p)?;
43		}
44		write!(f, ")")
45	}
46}
47
48impl Model {
49	#[cfg(feature = "ml")]
50	pub(crate) async fn compute(
51		&self,
52		ctx: &Context<'_>,
53		opt: &Options,
54		txn: &Transaction,
55		doc: Option<&CursorDoc<'_>>,
56	) -> Result<Value, Error> {
57		// Ensure futures are run
58		let opt = &opt.new_with_futures(true);
59		// Get the full name of this model
60		let name = format!("ml::{}", self.name);
61		// Check this function is allowed
62		ctx.check_allowed_function(name.as_str())?;
63		// Get the model definition
64		let val = {
65			// Claim transaction
66			let mut run = txn.lock().await;
67			// Get the function definition
68			run.get_and_cache_db_model(opt.ns(), opt.db(), &self.name, &self.version).await?
69		};
70		// Calculate the model path
71		let path = format!(
72			"ml/{}/{}/{}-{}-{}.surml",
73			opt.ns(),
74			opt.db(),
75			self.name,
76			self.version,
77			val.hash
78		);
79		// Check permissions
80		if opt.check_perms(Action::View) {
81			match &val.permissions {
82				Permission::Full => (),
83				Permission::None => {
84					return Err(Error::FunctionPermissions {
85						name: self.name.to_owned(),
86					})
87				}
88				Permission::Specific(e) => {
89					// Disable permissions
90					let opt = &opt.new_with_perms(false);
91					// Process the PERMISSION clause
92					if !e.compute(ctx, opt, txn, doc).await?.is_truthy() {
93						return Err(Error::FunctionPermissions {
94							name: self.name.to_owned(),
95						});
96					}
97				}
98			}
99		}
100		// Compute the function arguments
101		let mut args =
102			try_join_all(self.args.iter().map(|v| v.compute(ctx, opt, txn, doc))).await?;
103		// Check the minimum argument length
104		if args.len() != 1 {
105			return Err(Error::InvalidArguments {
106				name: format!("ml::{}<{}>", self.name, self.version),
107				message: ARGUMENTS.into(),
108			});
109		}
110		// Take the first and only specified argument
111		match args.swap_remove(0) {
112			// Perform bufferered compute
113			Value::Object(v) => {
114				// Compute the model function arguments
115				let mut args = v
116					.into_iter()
117					.map(|(k, v)| Ok((k, Value::try_into(v)?)))
118					.collect::<Result<HashMap<String, f32>, Error>>()
119					.map_err(|_| Error::InvalidArguments {
120						name: format!("ml::{}<{}>", self.name, self.version),
121						message: ARGUMENTS.into(),
122					})?;
123				// Get the model file as bytes
124				let bytes = crate::obs::get(&path).await?;
125				// Run the compute in a blocking task
126				let outcome = tokio::task::spawn_blocking(move || {
127					let mut file = SurMlFile::from_bytes(bytes).unwrap();
128					let compute_unit = ModelComputation {
129						surml_file: &mut file,
130					};
131					compute_unit.buffered_compute(&mut args).map_err(Error::ModelComputation)
132				})
133				.await
134				.unwrap()?;
135				// Convert the output to a value
136				Ok(outcome[0].into())
137			}
138			// Perform raw compute
139			Value::Number(v) => {
140				// Compute the model function arguments
141				let args: f32 = v.try_into().map_err(|_| Error::InvalidArguments {
142					name: format!("ml::{}<{}>", self.name, self.version),
143					message: ARGUMENTS.into(),
144				})?;
145				// Get the model file as bytes
146				let bytes = crate::obs::get(&path).await?;
147				// Convert the argument to a tensor
148				let tensor = ndarray::arr1::<f32>(&[args]).into_dyn();
149				// Run the compute in a blocking task
150				let outcome = tokio::task::spawn_blocking(move || {
151					let mut file = SurMlFile::from_bytes(bytes).unwrap();
152					let compute_unit = ModelComputation {
153						surml_file: &mut file,
154					};
155					compute_unit.raw_compute(tensor, None).map_err(Error::ModelComputation)
156				})
157				.await
158				.unwrap()?;
159				// Convert the output to a value
160				Ok(outcome[0].into())
161			}
162			// Perform raw compute
163			Value::Array(v) => {
164				// Compute the model function arguments
165				let args = v
166					.into_iter()
167					.map(Value::try_into)
168					.collect::<Result<Vec<f32>, Error>>()
169					.map_err(|_| Error::InvalidArguments {
170						name: format!("ml::{}<{}>", self.name, self.version),
171						message: ARGUMENTS.into(),
172					})?;
173				// Get the model file as bytes
174				let bytes = crate::obs::get(&path).await?;
175				// Convert the argument to a tensor
176				let tensor = ndarray::arr1::<f32>(&args).into_dyn();
177				// Run the compute in a blocking task
178				let outcome = tokio::task::spawn_blocking(move || {
179					let mut file = SurMlFile::from_bytes(bytes).unwrap();
180					let compute_unit = ModelComputation {
181						surml_file: &mut file,
182					};
183					compute_unit.raw_compute(tensor, None).map_err(Error::ModelComputation)
184				})
185				.await
186				.unwrap()?;
187				// Convert the output to a value
188				Ok(outcome[0].into())
189			}
190			//
191			_ => Err(Error::InvalidArguments {
192				name: format!("ml::{}<{}>", self.name, self.version),
193				message: ARGUMENTS.into(),
194			}),
195		}
196	}
197
198	#[cfg(not(feature = "ml"))]
199	pub(crate) async fn compute(
200		&self,
201		_ctx: &Context<'_>,
202		_opt: &Options,
203		_txn: &Transaction,
204		_doc: Option<&CursorDoc<'_>>,
205	) -> Result<Value, Error> {
206		Err(Error::InvalidModel {
207			message: String::from("Machine learning computation is not enabled."),
208		})
209	}
210}