surrealdb/api/engine/any/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
//! Dynamic support for any engine
//!
//! SurrealDB supports various ways of storing and accessing your data. For storing data we support a number of
//! key value stores. These are SurrealKV, RocksDB, TiKV, FoundationDB and an in-memory store. We call these
//! local engines. SurrealKV and RocksDB are file-based, single node key value stores. TiKV and FoundationDB are
//! are distributed stores that can scale horizontally across multiple nodes. The in-memory store does not persist
//! your data, it only stores it in memory. All these can be embedded in your application, so you don't need to
//! spin up a SurrealDB server first in order to use them. We also support spinning up a server externally and then
//! access your database via WebSockets or HTTP. We call these remote engines.
//!
//! The Rust SDK abstracts away the implementation details of the engines to make them work in a unified way.
//! All these engines, whether they are local or remote, work exactly the same way using the same API. The only
//! difference in the API is the endpoint you use to access the engine. Normally you provide the scheme of the engine
//! you want to use as a type parameter to `Surreal::new`. This allows you detect, at compile, whether the engine
//! you are trying to use is enabled. If not, your code won't compile. This is awesome but it strongly couples your
//! application to the engine you are using. In order to change an engine you would need to update your code to
//! the new scheme and endpoint you need to use and recompile it. This is where the `any` engine comes in. We will
//! call it `Surreal<Any>` (the type it creates) to avoid confusion with the word any.
//!
//! `Surreal<Any>` allows you to use any engine as long as it was enabled when compiling. Unlike with the typed scheme,
//! the choice of the engine is made at runtime depending on the endpoint that you provide as a string. If you use an
//! environment variable to provide this endpoint string, you won't need to change your code in order to
//! switch engines. The downside to this is that you will get a runtime error if you forget to enable the engine you
//! want to use when compiling your code. On the other hand, this totally decouples your application from the engine
//! you are using and makes it possible to use whichever engine SurrealDB supports by simply changing the Cargo
//! features you enable when compiling. This enables some cool workflows.
//!
//! One of the common use cases we see is using SurrealDB as an embedded database using RocksDB as the local engine.
//! This is a nice way to boost the performance of your application when all you need is a single node. The downside
//! of this approach is that RocksDB is not written in Rust so you will need to install some external dependencies
//! on your development machine in order to successfully compile it. Some of our users have reported that
//! this is not exactly straight-forward on Windows. Another issue is that RocksDB is very resource intensive to
//! compile and it takes a long time. Both of these issues can be easily avoided by using `Surreal<Any>`. You can
//! develop using an in-memory engine but deploy using RocksDB. If you develop on Windows but deploy to Linux then
//! you completely avoid having to build RocksDB on Windows at all.
//!
//! # Getting Started
//!
//! You can start by declaring your `surrealdb` dependency like this in Cargo.toml
//!
//! ```toml
//! surrealdb = {
//! version = "1",
//!
//! # Disables the default features, which are `protocol-ws` and `rustls`.
//! # Not necessary but can reduce your compile times if you don't need those features.
//! default-features = false,
//!
//! # Unconditionally enables the in-memory store.
//! # Also not necessary but this will make `cargo run` just work.
//! # Without it, you would need `cargo run --features surrealdb/kv-mem` during development. If you use a build
//! # tool like `make` or `cargo make`, however, you can put that in your build step and avoid typing it manually.
//! features = ["kv-mem"],
//!
//! # Also not necessary but this makes it easy to switch between `stable`, `beta` and `nightly` crates, if need be.
//! # See https://surrealdb.com/blog/introducing-nightly-and-beta-rust-crates for more information on those crates.
//! package = "surrealdb"
//! }
//! ```
//!
//! You then simply need to instantiate `Surreal<Any>` instead of `Surreal<Db>` or `Surreal<Client>`.
//!
//! # Examples
//!
//! ```rust
//! use std::env;
//! use surrealdb::engine::any;
//! use surrealdb::engine::any::Any;
//! use surrealdb::opt::Resource;
//! use surrealdb::Surreal;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Use the endpoint specified in the environment variable or default to `memory`.
//! // This makes it possible to use the memory engine during development but switch it
//! // to any other engine for deployment.
//! let endpoint = env::var("SURREALDB_ENDPOINT").unwrap_or_else(|_| "memory".to_owned());
//!
//! // Create the Surreal instance. This will create `Surreal<Any>`.
//! let db = any::connect(endpoint).await?;
//!
//! // Specify the namespace and database to use
//! db.use_ns("namespace").use_db("database").await?;
//!
//! // Use the database like you normally would.
//! delete_user(&db, "jane").await?;
//!
//! Ok(())
//! }
//!
//! // Deletes a user from the user table in the database
//! async fn delete_user(db: &Surreal<Any>, username: &str) -> surrealdb::Result<()> {
//! db.delete(Resource::from(("user", username))).await?;
//! Ok(())
//! }
//! ```
//!
//! By doing something like this, you can use an in-memory database on your development machine and you can just run `cargo run`
//! without having to specify the environment variable first or spinning up an external server remotely to avoid RocksDB's
//! compilation cost. You also don't need to install any `C` or `C++` dependencies on your Windows machine. For the production
//! binary you simply need to build it using something like
//!
//! ```bash
//! cargo build --features surrealdb/kv-rocksdb --release
//! ```
//!
//! and export the `SURREALDB_ENDPOINT` environment variable when starting it.
//!
//! ```bash
//! export SURREALDB_ENDPOINT="rocksdb:/path/to/database/folder"
//! /path/to/binary
//! ```
//!
//! The example above shows how you can avoid compiling RocksDB on your development machine, thereby avoiding dependency hell
//! and paying the compilation cost during development. This is not the only benefit you can derive from using `Surreal<Any>`
//! though. It's still useful even when your engine isn't expensive to compile. For example, the remote engines use pure Rust
//! dependencies but you can still benefit from using `Surreal<Any>` by using the in-memory engine for development and deploy
//! using a remote engine like the WebSocket engine. This way you avoid having to spin up a SurrealDB server first when
//! developing and testing your application.
//!
//! For some applications where you allow users to determine the engine they want to use, you can enable multiple engines for
//! them when building, or even enable them all. To do this you simply need to comma separate the Cargo features.
//!
//! ```bash
//! cargo build --features surrealdb/protocol-ws,surrealdb/kv-rocksdb,surrealdb/kv-tikv --release
//! ```
//!
//! In this case, the binary you build will have support for accessing an external server via WebSockets, embedding the database
//! using RocksDB or using a distributed TiKV cluster.
#[cfg(not(target_family = "wasm"))]
mod native;
#[cfg(target_family = "wasm")]
mod wasm;
use crate::api::err::Error;
use crate::api::opt::Config;
use crate::api::opt::Endpoint;
use crate::api::Connect;
use crate::api::Result;
use crate::api::Surreal;
use crate::opt::path_to_string;
use std::marker::PhantomData;
use std::sync::Arc;
use std::sync::OnceLock;
use tokio::sync::watch;
use url::Url;
/// A trait for converting inputs to a server address object
pub trait IntoEndpoint {
/// Converts an input into a server address object
fn into_endpoint(self) -> Result<Endpoint>;
}
fn split_url(url: &str) -> (&str, &str) {
match url.split_once("://") {
Some(parts) => parts,
None => match url.split_once(':') {
Some(parts) => parts,
None => (url, ""),
},
}
}
impl IntoEndpoint for &str {
fn into_endpoint(self) -> Result<Endpoint> {
let (url, path) = match self {
"memory" | "mem://" => (Url::parse("mem://").unwrap(), "memory".to_owned()),
url if url.starts_with("ws") | url.starts_with("http") | url.starts_with("tikv") => {
(Url::parse(url).map_err(|_| Error::InvalidUrl(self.to_owned()))?, String::new())
}
_ => {
let (scheme, path) = split_url(self);
let protocol = format!("{scheme}://");
(
Url::parse(&protocol).map_err(|_| Error::InvalidUrl(self.to_owned()))?,
path_to_string(&protocol, path),
)
}
};
let mut endpoint = Endpoint::new(url);
endpoint.path = path;
Ok(endpoint)
}
}
impl IntoEndpoint for &String {
fn into_endpoint(self) -> Result<Endpoint> {
self.as_str().into_endpoint()
}
}
impl IntoEndpoint for String {
fn into_endpoint(self) -> Result<Endpoint> {
self.as_str().into_endpoint()
}
}
impl<T> IntoEndpoint for (T, Config)
where
T: Into<String>,
{
fn into_endpoint(self) -> Result<Endpoint> {
let mut endpoint = IntoEndpoint::into_endpoint(self.0.into())?;
endpoint.config = self.1;
Ok(endpoint)
}
}
/// A dynamic connection that supports any engine and allows you to pick at runtime
#[derive(Debug, Clone)]
pub struct Any(());
impl Surreal<Any> {
/// Connects to a specific database endpoint, saving the connection on the static client
///
/// # Examples
///
/// ```no_run
/// use std::sync::LazyLock;
/// use surrealdb::Surreal;
/// use surrealdb::engine::any::Any;
///
/// static DB: LazyLock<Surreal<Any>> = LazyLock::new(Surreal::init);
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// DB.connect("ws://localhost:8000").await?;
/// # Ok(())
/// # }
/// ```
pub fn connect(&self, address: impl IntoEndpoint) -> Connect<Any, ()> {
Connect {
router: self.router.clone(),
engine: PhantomData,
address: address.into_endpoint(),
capacity: 0,
waiter: self.waiter.clone(),
response_type: PhantomData,
}
}
}
/// Connects to a local, remote or embedded database
///
/// # Examples
///
/// ```no_run
/// use surrealdb::engine::any::connect;
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// // Connect to a local endpoint
/// let db = connect("ws://localhost:8000").await?;
///
/// // Connect to a remote endpoint
/// let db = connect("wss://cloud.surrealdb.com").await?;
///
/// // Connect using HTTP
/// let db = connect("http://localhost:8000").await?;
///
/// // Connect using HTTPS
/// let db = connect("https://cloud.surrealdb.com").await?;
///
/// // Instantiate an in-memory instance
/// let db = connect("mem://").await?;
///
/// // Instantiate a file-backed instance (currently uses RocksDB)
/// let db = connect("file://path/to/database-folder").await?;
///
/// // Instantiate a RocksDB-backed instance
/// let db = connect("rocksdb://path/to/database-folder").await?;
///
/// // Instantiate a SurrealKV-backed instance
/// let db = connect("surrealkv://path/to/database-folder").await?;
///
/// // Instantiate an IndxDB-backed instance
/// let db = connect("indxdb://DatabaseName").await?;
///
/// // Instantiate a TiKV-backed instance
/// let db = connect("tikv://localhost:2379").await?;
///
/// // Instantiate a FoundationDB-backed instance
/// let db = connect("fdb://path/to/fdb.cluster").await?;
/// # Ok(())
/// # }
/// ```
pub fn connect(address: impl IntoEndpoint) -> Connect<Any, Surreal<Any>> {
Connect {
router: Arc::new(OnceLock::new()),
engine: PhantomData,
address: address.into_endpoint(),
capacity: 0,
waiter: Arc::new(watch::channel(None)),
response_type: PhantomData,
}
}
#[cfg(all(test, feature = "kv-mem"))]
mod tests {
use surrealdb_core::sql::Object;
use super::*;
use crate::opt::auth::Root;
use crate::opt::capabilities::Capabilities;
use crate::Value;
#[tokio::test]
async fn local_engine_without_auth() {
// Instantiate an in-memory instance without root credentials
let db = connect("memory").await.unwrap();
db.use_ns("N").use_db("D").await.unwrap();
// The client has access to everything
assert!(
db.query("INFO FOR ROOT").await.unwrap().check().is_ok(),
"client should have access to ROOT"
);
assert!(
db.query("INFO FOR NS").await.unwrap().check().is_ok(),
"client should have access to NS"
);
assert!(
db.query("INFO FOR DB").await.unwrap().check().is_ok(),
"client should have access to DB"
);
// There are no users in the datastore
let mut res = db.query("INFO FOR ROOT").await.unwrap();
let users: Value = res.take("users").unwrap();
assert_eq!(
users.into_inner(),
Object::default().into(),
"there should be no users in the system"
);
}
#[tokio::test]
async fn local_engine_with_auth() {
// Instantiate an in-memory instance with root credentials
let creds = Root {
username: "root",
password: "root",
};
let db = connect(("memory", Config::new().user(creds).capabilities(Capabilities::all())))
.await
.unwrap();
db.use_ns("N").use_db("D").await.unwrap();
// The client needs to sign in before it can access anything
assert!(
db.query("INFO FOR ROOT").await.unwrap().check().is_err(),
"client should not have access to KV"
);
assert!(
db.query("INFO FOR NS").await.unwrap().check().is_err(),
"client should not have access to NS"
);
assert!(
db.query("INFO FOR DB").await.unwrap().check().is_err(),
"client should not have access to DB"
);
// It can sign in
assert!(db.signin(creds).await.is_ok(), "client should be able to sign in");
// After the sign in, the client has access to everything
assert!(
db.query("INFO FOR ROOT").await.unwrap().check().is_ok(),
"client should have access to KV"
);
assert!(
db.query("INFO FOR NS").await.unwrap().check().is_ok(),
"client should have access to NS"
);
assert!(
db.query("INFO FOR DB").await.unwrap().check().is_ok(),
"client should have access to DB"
);
}
}