Integrate client library into surrealdb crate (#1514)

This commit is contained in:
Rushmore Mushambi 2022-12-30 10:23:19 +02:00 committed by GitHub
parent 54738ea4de
commit c2dce39f91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
142 changed files with 11238 additions and 234 deletions

View file

@ -15,6 +15,12 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Free disk space
uses: jlumbroso/free-disk-space@main
with:
large-packages: false
swap-storage: false
- name: Checkout sources
uses: actions/checkout@v3
@ -33,7 +39,9 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- name: Run cargo test
run: cargo test --workspace
run: |
(&>/dev/null cargo run -- start --log trace --user root --pass root memory &)
cargo test --workspace --features protocol-ws,protocol-http,kv-rocksdb
lint:
name: Lint
@ -57,6 +65,7 @@ jobs:
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
components: rustfmt, clippy
- name: Run cargo check --package surrealdb
@ -70,3 +79,15 @@ jobs:
- name: Run cargo clippy
run: cargo clippy -- -W warnings
- name: Run native cargo check --package surrealdb
run: cargo check --no-default-features --package surrealdb
- name: Run Wasm cargo check --package surrealdb
run: cargo check --features protocol-ws,protocol-http,kv-mem --target wasm32-unknown-unknown --package surrealdb
- name: Check IndxDB
run: cargo check --no-default-features --features kv-indxdb --target wasm32-unknown-unknown --package surrealdb
- name: Run cargo check --workspace
run: cargo check --workspace

1
.gitignore vendored
View file

@ -27,6 +27,7 @@ Temporary Items
**/*.rs.bk
*.db
*.sw?
# -----------------------------------
# Folders

643
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,8 @@ version = "1.0.0-beta.8"
authors = ["Tobie Morgan Hitchcock <tobie@surrealdb.com>"]
[features]
default = ["storage-rocksdb", "scripting", "http"]
default = ["storage-mem", "storage-rocksdb", "scripting", "http"]
storage-mem = ["surrealdb/kv-mem"]
storage-rocksdb = ["surrealdb/kv-rocksdb"]
storage-tikv = ["surrealdb/kv-tikv"]
storage-fdb = ["surrealdb/kv-fdb-6_3"]
@ -14,7 +15,7 @@ scripting = ["surrealdb/scripting"]
http = ["surrealdb/http"]
[workspace]
members = ["lib"]
members = ["lib", "lib/examples/actix"]
[profile.release]
lto = true
@ -43,7 +44,7 @@ serde = { version = "1.0.145", features = ["derive"] }
serde_cbor = "0.11.2"
serde_json = "1.0.85"
serde_pack = { version = "1.1.1", package = "rmp-serde" }
surrealdb = { path = "lib", default-features = false, features = ["kv-mem", "parallel"] }
surrealdb = { path = "lib", default-features = false }
thiserror = "1.0.37"
tokio = { version = "1.21.2", features = ["macros", "signal"] }
warp = { version = "0.3.3", features = ["compression", "tls", "websocket"] }

View file

@ -10,7 +10,7 @@ setup:
.PHONY: docs
docs:
cargo doc --open --no-deps --package surrealdb
cargo doc --open --no-deps --package surrealdb --features rustls,protocol-ws,protocol-http,kv-mem,kv-indxdb,kv-rocksdb,kv-tikv,http,scripting,parallel
.PHONY: test
test:

View file

@ -3,6 +3,7 @@ name = "surrealdb"
publish = true
edition = "2021"
version = "1.0.0-beta.8"
rust-version = "1.65.0"
readme = "CARGO.md"
authors = ["Tobie Morgan Hitchcock <tobie@surrealdb.com>"]
description = "A scalable, distributed, collaborative, document-graph database, for the realtime web"
@ -12,27 +13,42 @@ documentation = "https://docs.rs/surrealdb/"
keywords = ["database", "embedded-database", "key-value", "key-value-store", "kv-store"]
categories = ["database-implementations", "data-structures", "embedded"]
license = "Apache-2.0"
resolver = "2"
[features]
# Public features
default = ["parallel", "kv-mem", "kv-rocksdb", "scripting", "http"]
parallel = ["dep:executor"]
kv-mem = ["dep:echodb"]
default = ["protocol-ws", "rustls"]
protocol-http = ["dep:reqwest", "dep:tokio-util"]
protocol-ws = ["dep:tokio-tungstenite", "dep:tokio-stream", "tokio/time"]
kv-mem = ["dep:echodb", "parallel"]
kv-indxdb = ["dep:indxdb"]
kv-rocksdb = ["dep:rocksdb"]
kv-tikv = ["dep:tikv"]
kv-fdb-5_1 = ["foundationdb/fdb-5_1", "kv-fdb"]
kv-fdb-5_2 = ["foundationdb/fdb-5_2", "kv-fdb"]
kv-fdb-6_0 = ["foundationdb/fdb-6_0", "kv-fdb"]
kv-fdb-6_1 = ["foundationdb/fdb-6_1", "kv-fdb"]
kv-fdb-6_2 = ["foundationdb/fdb-6_2", "kv-fdb"]
kv-fdb-6_3 = ["foundationdb/fdb-6_3", "kv-fdb"]
kv-fdb-7_0 = ["foundationdb/fdb-7_0", "kv-fdb"]
kv-fdb-7_1 = ["foundationdb/fdb-7_1", "kv-fdb"]
kv-rocksdb = ["dep:rocksdb", "parallel"]
kv-tikv = ["dep:tikv", "parallel"]
kv-fdb-5_1 = ["foundationdb/fdb-5_1", "kv-fdb", "parallel"]
kv-fdb-5_2 = ["foundationdb/fdb-5_2", "kv-fdb", "parallel"]
kv-fdb-6_0 = ["foundationdb/fdb-6_0", "kv-fdb", "parallel"]
kv-fdb-6_1 = ["foundationdb/fdb-6_1", "kv-fdb", "parallel"]
kv-fdb-6_2 = ["foundationdb/fdb-6_2", "kv-fdb", "parallel"]
kv-fdb-6_3 = ["foundationdb/fdb-6_3", "kv-fdb", "parallel"]
kv-fdb-7_0 = ["foundationdb/fdb-7_0", "kv-fdb", "parallel"]
kv-fdb-7_1 = ["foundationdb/fdb-7_1", "kv-fdb", "parallel"]
scripting = ["dep:js", "dep:executor"]
http = ["dep:reqwest"]
native-tls = ["dep:native-tls", "reqwest?/native-tls", "tokio-tungstenite?/native-tls"]
rustls = ["dep:rustls", "reqwest?/rustls-tls", "tokio-tungstenite?/__rustls-tls"]
# Private features
kv-fdb = ["foundationdb"]
kv-fdb = ["foundationdb", "parallel"]
parallel = ["dep:executor"]
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [
"protocol-ws", "protocol-http",
"kv-mem", "kv-indxdb", "kv-rocksdb", "kv-tikv", "kv-fdb",
"rustls", "native-tls",
"http", "scripting"
]
targets = []
[dependencies]
addr = { version = "0.15.6", default-features = false, features = ["std"] }
@ -47,10 +63,13 @@ deunicode = "1.3.2"
dmp = "0.1.1"
echodb = { version = "0.3.0", optional = true }
executor = { version = "1.4.1", package = "async-executor", optional = true }
flume = "0.10.14"
futures = "0.3.24"
futures-concurrency = "7.0.0"
foundationdb = { version = "0.7.0", default-features = false, features = ["embedded-fdb-include"], optional = true }
fuzzy-matcher = "0.3.7"
geo = { version = "0.23.0", features = ["use-serde"] }
indexmap = { version = "1.9.2", features = ["serde"] }
indxdb = { version = "0.2.0", optional = true }
js = { version = "0.1.7", package = "rquickjs", features = ["bindgen", "classes", "futures", "loader", "macro", "properties", "parallel"], optional = true }
lexical-sort = "0.3.1"
@ -58,29 +77,42 @@ log = "0.4.17"
md-5 = "0.10.5"
msgpack = { version = "1.1.1", package = "rmp-serde" }
nanoid = "0.4.0"
native-tls = { version = "0.2.11", optional = true }
nom = { version = "7.1.1", features = ["alloc"] }
once_cell = "1.15.0"
once_cell = "1.16.0"
pbkdf2 = "0.11.0"
rand = "0.8.5"
regex = "1.6.0"
reqwest = { version = "0.11.13", default-features = false, features = ["json", "stream"], optional = true }
rocksdb = { version = "0.19.0", optional = true }
rustls = { version = "0.20.7", optional = true }
scrypt = "0.10.0"
semver = { version = "1.0.14", default-features = false }
serde = { version = "1.0.145", features = ["derive"] }
semver = { version = "1.0.14", features = ["serde"] }
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
sha-1 = "0.10.0"
sha2 = "0.10.6"
storekey = "0.3.0"
thiserror = "1.0.37"
tikv = { version = "0.1.0", package = "tikv-client", optional = true }
tokio-stream = { version = "0.1.11", optional = true }
tokio-util = { version = "0.7.4", optional = true, features = ["compat"] }
trice = "0.1.0"
url = "2.3.1"
[dev-dependencies]
tokio = { version = "1.21.2", features = ["macros", "rt", "rt-multi-thread"] }
time = { version = "0.3.17", features = ["serde"] }
tokio = { version = "1.22.0", features = ["macros", "rt", "rt-multi-thread"] }
ulid = { version = "1.0.0", features = ["serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
pharos = "0.5.3"
tokio = { version = "1.22.0", default-features = false, features = ["rt"] }
uuid = { version = "1.2.1", features = ["serde", "js", "v4", "v7"] }
wasm-bindgen-futures = "0.4.33"
ws_stream_wasm = "0.7.3"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.22.0", default-features = false, features = ["io-util", "fs", "rt-multi-thread"] }
tokio-tungstenite = { version = "0.17.2", optional = true }
uuid = { version = "1.2.1", features = ["serde", "v4", "v7"] }

146
lib/README.md Normal file
View file

@ -0,0 +1,146 @@
# surrealdb
The official SurrealDB library for Rust.
[![](https://img.shields.io/badge/status-beta-ff00bb.svg?style=flat-square)](https://github.com/surrealdb/surrealdb) [![](https://img.shields.io/badge/docs-view-44cc11.svg?style=flat-square)](https://surrealdb.com/docs/integration/libraries/rust) [![](https://img.shields.io/badge/license-Apache_License_2.0-00bfff.svg?style=flat-square)](https://github.com/surrealdb/surrealdb)
<h2><img height="20" src="https://github.com/surrealdb/surrealdb/blob/main/img/whatissurreal.svg?raw=true">&nbsp;&nbsp;What is SurrealDB?</h2>
SurrealDB is an end-to-end cloud native database for web, mobile, serverless, jamstack, backend, and traditional applications. SurrealDB reduces the development time of modern applications by simplifying your database and API stack, removing the need for most server-side components, allowing you to build secure, performant apps quicker and cheaper. SurrealDB acts as both a database and a modern, realtime, collaborative API backend layer. SurrealDB can run as a single server or in a highly-available, highly-scalable distributed mode - with support for SQL querying from client devices, GraphQL, ACID transactions, WebSocket connections, structured and unstructured data, graph querying, full-text indexing, geospatial querying, and row-by-row permissions-based access.
View the [features](https://surrealdb.com/features), the latest [releases](https://surrealdb.com/releases), the product [roadmap](https://surrealdb.com/roadmap), and [documentation](https://surrealdb.com/docs).
<h2><img height="20" src="https://github.com/surrealdb/surrealdb/blob/main/img/features.svg?raw=true">&nbsp;&nbsp;Features</h2>
- [x] Can be used as an embedded database (`Surreal<Db>`)
- [x] Connects to remote servers (`Surreal<ws::Client>` or `Surreal<http::Client>`)
- [x] Allows picking any protocol or storage engine at run-time (`Surreal<Any>`)
- [x] Compiles to WebAssembly
- [x] Supports typed SQL statements
- [x] Invalid SQL queries are never sent to the server, the client uses the same parser the server uses
- [x] Static clients, no need for `once_cell` or `lazy_static`
- [x] Clonable connections with auto-reconnect capabilities, no need for a connection pool
- [x] Range queries
- [x] Consistent API across all supported protocols and storage engines
- [x] Asynchronous, lock-free connections
- [x] TLS support via either [`rustls`](https://crates.io/crates/rustls) or [`native-tls`](https://crates.io/crates/native-tls)
<h2><img height="20" src="https://github.com/surrealdb/surrealdb/blob/main/img/installation.svg?raw=true">&nbsp;&nbsp;Installation</h2>
To add this crate as a Rust dependency, simply run
```bash
cargo add surrealdb
```
**IMPORTANT**: The client supports SurrealDB `v1.0.0-beta.8+20221030.c12a1cc` or later. So please make sure you have that or a newer version of the server before proceeding. For now, that means a recent nightly version.
<h2><img height="20" src="https://github.com/surrealdb/surrealdb/blob/main/img/features.svg?raw=true">&nbsp;&nbsp;Quick look</h2>
This library enables simple and advanced querying of an embedded or remote database from server-side or client-side (via Wasm) code. By default, all remote connections to SurrealDB are made over WebSockets, and automatically reconnect when the connection is terminated. Connections are automatically closed when they get dropped.
```rust
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::borrow::Cow;
use surrealdb::sql;
use surrealdb::Surreal;
use surrealdb::engines::remote::ws::Ws;
use surrealdb::opt::auth::Root;
#[derive(Serialize, Deserialize)]
struct Name {
first: Cow<'static, str>,
last: Cow<'static, str>,
}
#[derive(Serialize, Deserialize)]
struct Person {
#[serde(skip_serializing)]
id: Option<String>,
title: Cow<'static, str>,
name: Name,
marketing: bool,
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("localhost:8000").await?;
// Signin as a namespace, database, or root user
db.signin(Root {
username: "root",
password: "root",
})
.await?;
// Select a specific namespace and database
db.use_ns("namespace").use_db("database").await?;
// Create a new person with a random ID
let tobie: Person = db
.create("person")
.content(Person {
id: None,
title: "Founder & CEO".into(),
name: Name {
first: "Tobie".into(),
last: "Morgan Hitchcock".into(),
},
marketing: true,
})
.await?;
assert!(tobie.id.is_some());
// Create a new person with a specific ID
let mut jaime: Person = db
.create(("person", "jaime"))
.content(Person {
id: None,
title: "Founder & COO".into(),
name: Name {
first: "Jaime".into(),
last: "Morgan Hitchcock".into(),
},
marketing: false,
})
.await?;
assert_eq!(jaime.id.unwrap(), "person:jaime");
// Update a person record with a specific ID
jaime = db
.update(("person", "jaime"))
.merge(json!({ "marketing": true }))
.await?;
assert!(jaime.marketing);
// Select all people records
let people: Vec<Person> = db.select("person").await?;
assert!(!people.is_empty());
// Perform a custom advanced query
let sql = sql! {
SELECT marketing, count()
FROM type::table($table)
GROUP BY marketing
};
let groups = db.query(sql)
.bind(("table", "person"))
.await?;
dbg!(groups);
// Delete all people upto but not including Jaime
db.delete("person").range(.."jaime").await?;
// Delete all people
db.delete("person").await?;
Ok(())
}
```

View file

@ -0,0 +1,11 @@
[package]
name = "actix-example"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
actix-web = { version = "4.2.1", features = ["macros"] }
serde = { version = "1.0.147", features = ["derive"] }
surrealdb = { path = "../.." }
thiserror = "1.0.37"

View file

@ -0,0 +1,23 @@
use actix_web::{HttpResponse, ResponseError};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("database error")]
Db,
}
impl ResponseError for Error {
fn error_response(&self) -> HttpResponse {
match self {
Error::Db => HttpResponse::InternalServerError().body(self.to_string()),
}
}
}
impl From<surrealdb::Error> for Error {
fn from(error: surrealdb::Error) -> Self {
eprintln!("{error}");
Self::Db
}
}

View file

@ -0,0 +1,37 @@
mod error;
mod person;
use actix_web::{App, HttpServer};
use surrealdb::engines::remote::ws::Client;
use surrealdb::engines::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::Surreal;
static DB: Surreal<Client> = Surreal::init();
#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
DB.connect::<Ws>("localhost:8000").await?;
DB.signin(Root {
username: "root",
password: "root",
})
.await?;
DB.use_ns("namespace").use_db("database").await?;
HttpServer::new(|| {
App::new()
.service(person::create)
.service(person::read)
.service(person::update)
.service(person::delete)
.service(person::list)
})
.bind(("localhost", 8080))?
.run()
.await?;
Ok(())
}

View file

@ -0,0 +1,44 @@
use crate::error::Error;
use crate::DB;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{delete, get, post, put};
use serde::Deserialize;
use serde::Serialize;
const PERSON: &str = "person";
#[derive(Serialize, Deserialize)]
pub struct Person {
name: String,
}
#[post("/person/{id}")]
pub async fn create(id: Path<String>, person: Json<Person>) -> Result<Json<Person>, Error> {
let person = DB.create((PERSON, &*id)).content(person).await?;
Ok(Json(person))
}
#[get("/person/{id}")]
pub async fn read(id: Path<String>) -> Result<Json<Option<Person>>, Error> {
let person = DB.select((PERSON, &*id)).await?;
Ok(Json(person))
}
#[put("/person/{id}")]
pub async fn update(id: Path<String>, person: Json<Person>) -> Result<Json<Person>, Error> {
let person = DB.update((PERSON, &*id)).content(person).await?;
Ok(Json(person))
}
#[delete("/person/{id}")]
pub async fn delete(id: Path<String>) -> Result<Json<()>, Error> {
DB.delete((PERSON, &*id)).await?;
Ok(Json(()))
}
#[get("/people")]
pub async fn list() -> Result<Json<Vec<Person>>, Error> {
let people = DB.select(PERSON).await?;
Ok(Json(people))
}

View file

@ -0,0 +1,37 @@
use surrealdb::engines::remote::ws::Client;
use surrealdb::engines::remote::ws::Ws;
use surrealdb::Surreal;
use tokio::sync::mpsc;
static DB: Surreal<Client> = Surreal::init();
const NUM: usize = 100_000;
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
DB.connect::<Ws>("localhost:8000").with_capacity(NUM).await?;
DB.use_ns("namespace").use_db("database").await?;
let (tx, mut rx) = mpsc::channel::<()>(1);
for idx in 0..NUM {
let sender = tx.clone();
tokio::spawn(async move {
let mut result = DB.query("SELECT * FROM $idx").bind(("idx", idx)).await.unwrap();
let db_idx: Option<usize> = result.take(0).unwrap();
if let Some(db_idx) = db_idx {
println!("{idx}: {db_idx}");
}
drop(sender);
});
}
drop(tx);
rx.recv().await;
Ok(())
}

View file

@ -0,0 +1,54 @@
use serde::Deserialize;
use serde::Serialize;
use surrealdb::engines::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::sql;
use surrealdb::Surreal;
#[derive(Debug, Serialize, Deserialize)]
#[allow(dead_code)]
struct User {
id: String,
name: String,
company: String,
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("localhost:8000").await?;
db.signin(Root {
username: "root",
password: "root",
})
.await?;
db.use_ns("namespace").use_db("database").await?;
let sql = sql! {
CREATE user
SET name = $name,
company = $company
};
let mut results = db
.query(sql)
.bind(User {
id: "john".to_owned(),
name: "John Doe".to_owned(),
company: "ACME Corporation".to_owned(),
})
.await?;
// print the created user:
let user: Option<User> = results.take(0)?;
println!("{user:?}");
let mut response = db.query(sql!(SELECT * FROM user WHERE name.first = "John")).await?;
// print all users:
let users: Vec<User> = response.take(0)?;
println!("{users:?}");
Ok(())
}

View file

@ -0,0 +1,32 @@
use serde::Deserialize;
use surrealdb::engines::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::Surreal;
const ACCOUNT: &str = "account";
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Account {
id: String,
balance: String,
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("localhost:8000").await?;
db.signin(Root {
username: "root",
password: "root",
})
.await?;
db.use_ns("namespace").use_db("database").await?;
let accounts: Vec<Account> = db.select(ACCOUNT).range("one".."two").await?;
println!("{accounts:?}");
Ok(())
}

View file

@ -0,0 +1,45 @@
use surrealdb::engines::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::sql::statements::BeginStatement;
use surrealdb::sql::statements::CommitStatement;
use surrealdb::Surreal;
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("localhost:8000").await?;
db.signin(Root {
username: "root",
password: "root",
})
.await?;
db.use_ns("namespace").use_db("database").await?;
#[rustfmt::skip]
let response = db
// Start transaction
.query(BeginStatement)
// Setup accounts
.query("
CREATE account:one SET balance = 135605.16;
CREATE account:two SET balance = 91031.31;
")
// Move money
.query("
UPDATE account:one SET balance += 300.00;
UPDATE account:two SET balance -= 300.00;
")
// Finalise
.query(CommitStatement)
.await?;
// See if any errors were returned
response.check()?;
Ok(())
}

View file

@ -0,0 +1,13 @@
use surrealdb::engines::remote::ws::Ws;
use surrealdb::Surreal;
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("localhost:8000").await?;
let version = db.version().await?;
println!("{version:?}");
Ok(())
}

215
lib/src/api/conn.rs Normal file
View file

@ -0,0 +1,215 @@
use crate::api;
use crate::api::method::query::Response;
use crate::api::opt::Endpoint;
use crate::api::ExtraFeatures;
use crate::api::Result;
use crate::api::Surreal;
use crate::sql::Query;
use crate::sql::Value;
use flume::Receiver;
use flume::Sender;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::Ordering;
#[derive(Debug)]
#[allow(dead_code)] // used by the embedded and remote connections
pub(crate) struct Route {
pub(crate) request: (i64, Method, Param),
pub(crate) response: Sender<Result<DbResponse>>,
}
/// Message router
#[derive(Debug)]
pub struct Router<C: api::Connection> {
pub(crate) conn: PhantomData<C>,
pub(crate) sender: Sender<Option<Route>>,
pub(crate) last_id: AtomicI64,
pub(crate) features: HashSet<ExtraFeatures>,
}
impl<C> Router<C>
where
C: api::Connection,
{
pub(crate) fn next_id(&self) -> i64 {
self.last_id.fetch_add(1, Ordering::SeqCst)
}
}
impl<C> Drop for Router<C>
where
C: api::Connection,
{
fn drop(&mut self) {
let _res = self.sender.send(None);
}
}
/// The query method
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Method {
/// Sends an authentication token to the server
Authenticate,
/// Perfoms a merge update operation
Merge,
/// Creates a record in a table
Create,
/// Deletes a record from a table
Delete,
/// Exports a database
Export,
/// Checks the health of the server
Health,
/// Imports a database
Import,
/// Invalidates a session
Invalidate,
/// Kills a live query
#[doc(hidden)] // Not supported yet
Kill,
/// Starts a live query
#[doc(hidden)] // Not supported yet
Live,
/// Perfoms a patch update operation
Patch,
/// Sends a raw query to the database
Query,
/// Selects a record or records from a table
Select,
/// Sets a parameter on the connection
Set,
/// Signs into the server
Signin,
/// Signs up on the server
Signup,
/// Removes a parameter from a connection
Unset,
/// Perfoms an update operation
Update,
/// Selects a namespace and database to use
Use,
/// Queries the version of the server
Version,
}
/// The database response sent from the router to the caller
#[derive(Debug)]
pub enum DbResponse {
/// The response sent for the `query` method
Query(Response),
/// The response sent for any method except `query`
Other(Value),
}
/// Holds the parameters given to the caller
#[derive(Debug)]
#[allow(dead_code)] // used by the embedded and remote connections
pub struct Param {
pub(crate) query: Option<(Query, BTreeMap<String, Value>)>,
pub(crate) other: Vec<Value>,
pub(crate) file: Option<PathBuf>,
}
impl Param {
pub(crate) fn new(other: Vec<Value>) -> Self {
Self {
other,
query: None,
file: None,
}
}
pub(crate) fn query(query: Query, bindings: BTreeMap<String, Value>) -> Self {
Self {
query: Some((query, bindings)),
other: Vec::new(),
file: None,
}
}
pub(crate) fn file(file: PathBuf) -> Self {
Self {
query: None,
other: Vec::new(),
file: Some(file),
}
}
}
/// Connection trait implemented by supported protocols
pub trait Connection: Sized + Send + Sync + 'static {
/// Constructs a new client without connecting to the server
fn new(method: Method) -> Self;
/// Connect to the server
fn connect(
address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>>
where
Self: api::Connection;
/// Send a query to the server
#[allow(clippy::type_complexity)]
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>>
where
Self: api::Connection;
/// Receive responses for all methods except `query`
fn recv<R>(
&mut self,
receiver: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned;
/// Receive the response of the `query` method
fn recv_query(
&mut self,
receiver: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + '_>>;
/// Execute all methods except `query`
fn execute<'r, R>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + 'r>>
where
R: DeserializeOwned,
Self: api::Connection,
{
Box::pin(async move {
let rx = self.send(router, param).await?;
self.recv(rx).await
})
}
/// Execute the `query` method
fn execute_query<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + 'r>>
where
Self: api::Connection,
{
Box::pin(async move {
let rx = self.send(router, param).await?;
self.recv_query(rx).await
})
}
}

View file

@ -0,0 +1,303 @@
//! Dynamic support for any engine
//!
//! # Examples
//!
//! ```no_run
//! use serde::{Serialize, Deserialize};
//! use serde_json::json;
//! use std::borrow::Cow;
//! use surrealdb::sql;
//! use surrealdb::engines::any::connect;
//! use surrealdb::opt::auth::Root;
//!
//! #[derive(Serialize, Deserialize)]
//! struct Name {
//! first: Cow<'static, str>,
//! last: Cow<'static, str>,
//! }
//!
//! #[derive(Serialize, Deserialize)]
//! struct Person {
//! title: Cow<'static, str>,
//! name: Name,
//! marketing: bool,
//! }
//!
//! #[tokio::main]
//! async fn main() -> surrealdb::Result<()> {
//! let db = connect("ws://localhost:8000").await?;
//!
//! // Signin as a namespace, database, or root user
//! db.signin(Root {
//! username: "root",
//! password: "root",
//! }).await?;
//!
//! // Select a specific namespace / database
//! db.use_ns("namespace").use_db("database").await?;
//!
//! // Create a new person with a random ID
//! let created: Person = db.create("person")
//! .content(Person {
//! title: "Founder & CEO".into(),
//! name: Name {
//! first: "Tobie".into(),
//! last: "Morgan Hitchcock".into(),
//! },
//! marketing: true,
//! })
//! .await?;
//!
//! // Create a new person with a specific ID
//! let created: Person = db.create(("person", "jaime"))
//! .content(Person {
//! title: "Founder & COO".into(),
//! name: Name {
//! first: "Jaime".into(),
//! last: "Morgan Hitchcock".into(),
//! },
//! marketing: false,
//! })
//! .await?;
//!
//! // Update a person record with a specific ID
//! let updated: Person = db.update(("person", "jaime"))
//! .merge(json!({"marketing": true}))
//! .await?;
//!
//! // Select all people records
//! let people: Vec<Person> = db.select("person").await?;
//!
//! // Perform a custom advanced query
//! let sql = sql! {
//! SELECT marketing, count()
//! FROM type::table($table)
//! GROUP BY marketing
//! };
//!
//! let groups = db.query(sql)
//! .bind(("table", "person"))
//! .await?;
//!
//! Ok(())
//! }
//! ```
#[cfg(not(target_arch = "wasm32"))]
mod native;
#[cfg(target_arch = "wasm32")]
mod wasm;
use crate::api::conn::Method;
use crate::api::err::Error;
use crate::api::opt::Endpoint;
#[cfg(any(
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-fdb",
feature = "kv-indxdb",
))]
use crate::api::opt::Strict;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use crate::api::opt::Tls;
use crate::api::Connect;
use crate::api::Result;
use crate::api::Surreal;
use std::marker::PhantomData;
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>;
}
impl IntoEndpoint for &str {
fn into_endpoint(self) -> Result<Endpoint> {
Ok(Endpoint {
endpoint: Url::parse(self).map_err(|_| Error::InvalidUrl(self.to_owned()))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
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> {
Ok(Endpoint {
endpoint: Url::parse(&self).map_err(|_| Error::InvalidUrl(self))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
impl<T> IntoEndpoint for (T, rustls::ClientConfig)
where
T: Into<String>,
{
fn into_endpoint(self) -> Result<Endpoint> {
let (address, config) = self;
let mut address = address.into().into_endpoint()?;
address.tls_config = Some(Tls::Rust(config));
Ok(address)
}
}
#[cfg(any(
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-fdb",
feature = "kv-indxdb",
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-fdb",
feature = "kv-indxdb",
)))
)]
impl<T> IntoEndpoint for (T, Strict)
where
T: Into<String>,
{
fn into_endpoint(self) -> Result<Endpoint> {
let mut address = IntoEndpoint::into_endpoint(self.0.into())?;
address.strict = true;
Ok(address)
}
}
#[cfg(all(
any(
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-fdb",
feature = "kv-indxdb",
),
feature = "rustls",
))]
#[cfg_attr(
docsrs,
doc(cfg(all(
any(
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-fdb",
feature = "kv-indxdb",
),
feature = "rustls",
)))
)]
impl<T> IntoEndpoint for (T, rustls::ClientConfig, Strict)
where
T: Into<String>,
{
fn into_endpoint(self) -> Result<Endpoint> {
let (address, config, _) = self;
let mut address = address.into().into_endpoint()?;
address.tls_config = Some(Tls::Rust(config));
address.strict = true;
Ok(address)
}
}
/// A dynamic connection that supports any engine and allows you to pick at runtime
#[derive(Debug)]
pub struct Any {
id: i64,
method: Method,
}
impl Surreal<Any> {
/// Connects to a specific database endpoint, saving the connection on the static client
///
/// # Examples
///
/// ```no_run
/// use surrealdb::Surreal;
/// use surrealdb::engines::any::Any;
///
/// static DB: Surreal<Any> = Surreal::init();
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// DB.connect("ws://localhost:8000").await?;
/// # Ok(())
/// # }
/// ```
pub fn connect(&'static self, address: impl IntoEndpoint) -> Connect<Any, ()> {
Connect {
router: Some(&self.router),
address: address.into_endpoint(),
capacity: 0,
client: PhantomData,
response_type: PhantomData,
}
}
}
/// Connects to a local, remote or embedded database
///
/// # Examples
///
/// ```no_run
/// use surrealdb::engines::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 an file-backed instance
/// let db = connect("file://temp.db").await?;
///
/// // Instantiate an IndxDB-backed instance
/// let db = connect("indxdb://MyDatabase").await?;
///
/// // Instantiate a TiKV-backed instance
/// let db = connect("tikv://localhost:2379").await?;
///
/// // Instantiate a FoundationDB-backed instance
/// let db = connect("fdb://fdb.cluster").await?;
/// # Ok(())
/// # }
/// ```
pub fn connect(address: impl IntoEndpoint) -> Connect<'static, Any, Surreal<Any>> {
Connect {
router: None,
address: address.into_endpoint(),
capacity: 0,
client: PhantomData,
response_type: PhantomData,
}
}

View file

@ -0,0 +1,224 @@
use crate::api::conn::Connection;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
#[allow(unused_imports)] // used by the DB engines
use crate::api::engines;
use crate::api::engines::any::Any;
use crate::api::err::Error;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(feature = "protocol-http")]
use crate::api::opt::Tls;
use crate::api::DbResponse;
#[allow(unused_imports)] // used by the DB engines
use crate::api::ExtraFeatures;
use crate::api::Response;
use crate::api::Result;
use crate::api::Surreal;
use flume::Receiver;
use once_cell::sync::OnceCell;
#[cfg(feature = "protocol-http")]
use reqwest::header::HeaderMap;
#[cfg(feature = "protocol-http")]
use reqwest::header::HeaderValue;
#[cfg(feature = "protocol-http")]
use reqwest::header::ACCEPT;
#[cfg(feature = "protocol-http")]
use reqwest::ClientBuilder;
use serde::de::DeserializeOwned;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
#[cfg(feature = "protocol-ws")]
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
#[cfg(feature = "protocol-ws")]
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use tokio_tungstenite::Connector;
impl crate::api::Connection for Any {}
impl Connection for Any {
fn new(method: Method) -> Self {
Self {
method,
id: 0,
}
}
#[allow(unused_variables, unreachable_code, unused_mut)] // these are all used depending on feature
fn connect(
address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
let (route_tx, route_rx) = match capacity {
0 => flume::unbounded(),
capacity => flume::bounded(capacity),
};
let (conn_tx, conn_rx) = flume::bounded::<Result<()>>(1);
let mut features = HashSet::new();
match address.endpoint.scheme() {
#[cfg(feature = "kv-fdb")]
"fdb" => {
features.insert(ExtraFeatures::Backup);
engines::local::native::router(address, conn_tx, route_rx);
conn_rx.into_recv_async().await??
}
#[cfg(feature = "kv-mem")]
"mem" => {
features.insert(ExtraFeatures::Backup);
engines::local::native::router(address, conn_tx, route_rx);
conn_rx.into_recv_async().await??
}
#[cfg(feature = "kv-rocksdb")]
"rocksdb" => {
features.insert(ExtraFeatures::Backup);
engines::local::native::router(address, conn_tx, route_rx);
conn_rx.into_recv_async().await??
}
#[cfg(feature = "kv-rocksdb")]
"file" => {
features.insert(ExtraFeatures::Backup);
engines::local::native::router(address, conn_tx, route_rx);
conn_rx.into_recv_async().await??
}
#[cfg(feature = "kv-tikv")]
"tikv" => {
features.insert(ExtraFeatures::Backup);
engines::local::native::router(address, conn_tx, route_rx);
conn_rx.into_recv_async().await??
}
#[cfg(feature = "protocol-http")]
"http" | "https" => {
features.insert(ExtraFeatures::Auth);
features.insert(ExtraFeatures::Backup);
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
#[allow(unused_mut)]
let mut builder = ClientBuilder::new().default_headers(headers);
#[cfg(any(feature = "native-tls", feature = "rustls"))]
if let Some(tls) = address.tls_config {
builder = match tls {
#[cfg(feature = "native-tls")]
Tls::Native(config) => builder.use_preconfigured_tls(config),
#[cfg(feature = "rustls")]
Tls::Rust(config) => builder.use_preconfigured_tls(config),
};
}
let client = builder.build()?;
let base_url = address.endpoint;
engines::remote::http::health(
client.get(base_url.join(Method::Health.as_str())?),
)
.await?;
engines::remote::http::native::router(base_url, client, route_rx);
}
#[cfg(feature = "protocol-ws")]
"ws" | "wss" => {
features.insert(ExtraFeatures::Auth);
let url = address.endpoint.join(engines::remote::ws::PATH)?;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
let maybe_connector = address.tls_config.map(Connector::from);
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
let maybe_connector = None;
let config = WebSocketConfig {
max_send_queue: match capacity {
0 => None,
capacity => Some(capacity),
},
max_message_size: Some(engines::remote::ws::native::MAX_MESSAGE_SIZE),
max_frame_size: Some(engines::remote::ws::native::MAX_FRAME_SIZE),
accept_unmasked_frames: false,
};
let socket = engines::remote::ws::native::connect(
&url,
Some(config),
maybe_connector.clone(),
)
.await?;
engines::remote::ws::native::router(
url,
maybe_connector,
capacity,
config,
socket,
route_rx,
);
}
scheme => {
return Err(Error::Scheme(scheme.to_owned()).into());
}
}
Ok(Surreal {
router: OnceCell::with_value(Arc::new(Router {
features,
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
})),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
let (sender, receiver) = flume::bounded(1);
self.id = router.next_id();
let route = Route {
request: (self.id, self.method, param),
response: sender,
};
router.sender.send_async(Some(route)).await?;
Ok(receiver)
})
}
fn recv<R>(
&mut self,
receiver: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let response = receiver.into_recv_async().await?;
match response? {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
receiver: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + '_>> {
Box::pin(async move {
let response = receiver.into_recv_async().await?;
match response? {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}

View file

@ -0,0 +1,179 @@
use crate::api::conn::Connection;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
#[allow(unused_imports)] // used by the DB engines
use crate::api::engines;
use crate::api::engines::any::Any;
use crate::api::err::Error;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
use crate::api::DbResponse;
#[allow(unused_imports)] // used by the `ws` and `http` protocols
use crate::api::ExtraFeatures;
use crate::api::Response;
use crate::api::Result;
use crate::api::Surreal;
use flume::Receiver;
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
impl crate::api::Connection for Any {}
impl Connection for Any {
fn new(method: Method) -> Self {
Self {
method,
id: 0,
}
}
#[allow(unused_variables, unreachable_code, unused_mut)] // these are all used depending on feature
fn connect(
address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
let (route_tx, route_rx) = match capacity {
0 => flume::unbounded(),
capacity => flume::bounded(capacity),
};
let (conn_tx, conn_rx) = flume::bounded::<Result<()>>(1);
let mut features = HashSet::new();
match address.endpoint.scheme() {
#[cfg(feature = "kv-fdb")]
"fdb" => {
engines::local::wasm::router(address, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
}
#[cfg(feature = "kv-indxdb")]
"indxdb" => {
engines::local::wasm::router(address, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
}
#[cfg(feature = "kv-mem")]
"mem" => {
engines::local::wasm::router(address, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
}
#[cfg(feature = "kv-rocksdb")]
"rocksdb" => {
engines::local::wasm::router(address, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
}
#[cfg(feature = "kv-rocksdb")]
"file" => {
engines::local::wasm::router(address, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
}
#[cfg(feature = "kv-tikv")]
"tikv" => {
engines::local::wasm::router(address, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
}
#[cfg(feature = "protocol-http")]
"http" | "https" => {
features.insert(ExtraFeatures::Auth);
engines::remote::http::wasm::router(address, conn_tx, route_rx);
}
#[cfg(feature = "protocol-ws")]
"ws" | "wss" => {
features.insert(ExtraFeatures::Auth);
let mut address = address;
address.endpoint = address.endpoint.join(engines::remote::ws::PATH)?;
engines::remote::ws::wasm::router(address, capacity, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
}
scheme => {
return Err(Error::Scheme(scheme.to_owned()).into());
}
}
Ok(Surreal {
router: OnceCell::with_value(Arc::new(Router {
features,
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
})),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
let (sender, receiver) = flume::bounded(1);
self.id = router.next_id();
let route = Route {
request: (self.id, self.method, param),
response: sender,
};
router.sender.send_async(Some(route)).await?;
Ok(receiver)
})
}
fn recv<R>(
&mut self,
receiver: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let response = receiver.into_recv_async().await?;
match response? {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
receiver: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + '_>> {
Box::pin(async move {
let response = receiver.into_recv_async().await?;
match response? {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}

View file

@ -0,0 +1,556 @@
//! Embedded database instance
//!
//! `SurrealDB` itself can be embedded in this library, allowing you to query it using the same
//! crate and API that you would use when connecting to it remotely via WebSockets or HTTP.
//! All storage engines are supported but you have to activate their feature
//! flags first.
//!
//! **NB**: Some storage engines like `TiKV` and `RocksDB` depend on non-Rust libraries so you need
//! to install those libraries before you can build this crate when you activate their feature
//! flags. Please refer to [these instructions](https://github.com/surrealdb/surrealdb/blob/main/doc/BUILDING.md)
//! for more details on how to install them. If you are on Linux and you use
//! [the Nix package manager](https://github.com/surrealdb/surrealdb/tree/main/pkg/nix#installing-nix)
//! you can just run
//!
//! ```bash
//! nix develop github:surrealdb/surrealdb
//! ```
//!
//! which will drop you into a shell with all the dependencies available. One tip you may find
//! useful is to only enable the in-memory engine (`kv-mem`) during development. Besides letting you not
//! worry about those dependencies on your dev machine, it allows you to keep compile times low
//! during development while allowing you to test your code fully.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod native;
#[cfg(target_arch = "wasm32")]
pub(crate) mod wasm;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::engines::create_statement;
use crate::api::engines::delete_statement;
use crate::api::engines::merge_statement;
use crate::api::engines::patch_statement;
use crate::api::engines::select_statement;
use crate::api::engines::update_statement;
#[cfg(not(target_arch = "wasm32"))]
use crate::api::err::Error;
use crate::api::Connect;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
#[cfg(not(target_arch = "wasm32"))]
use crate::channel;
use crate::dbs::Response;
use crate::dbs::Session;
use crate::kvs::Datastore;
use crate::opt::IntoEndpoint;
use crate::sql::Array;
use crate::sql::Query;
use crate::sql::Statement;
use crate::sql::Statements;
use crate::sql::Strand;
use crate::sql::Value;
use indexmap::IndexMap;
use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::mem;
#[cfg(not(target_arch = "wasm32"))]
use tokio::fs::OpenOptions;
#[cfg(not(target_arch = "wasm32"))]
use tokio::io;
#[cfg(not(target_arch = "wasm32"))]
use tokio::io::AsyncReadExt;
#[cfg(not(target_arch = "wasm32"))]
use tokio::io::AsyncWriteExt;
const LOG: &str = "surrealdb::api::engines::local";
/// In-memory database
///
/// # Examples
///
/// Instantiating a global instance
///
/// ```
/// use surrealdb::{Result, Surreal};
/// use surrealdb::engines::local::Db;
/// use surrealdb::engines::local::Mem;
///
/// static DB: Surreal<Db> = Surreal::init();
///
/// #[tokio::main]
/// async fn main() -> Result<()> {
/// DB.connect::<Mem>(()).await?;
///
/// Ok(())
/// }
/// ```
///
/// Instantiating an in-memory instance
///
/// ```
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::Mem;
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// let db = Surreal::new::<Mem>(()).await?;
/// # Ok(())
/// # }
/// ```
///
/// Instantiating an in-memory strict instance
///
/// ```
/// use surrealdb::opt::Strict;
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::Mem;
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// let db = Surreal::new::<Mem>(Strict).await?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "kv-mem")]
#[cfg_attr(docsrs, doc(cfg(feature = "kv-mem")))]
#[derive(Debug)]
pub struct Mem;
/// File database
///
/// # Examples
///
/// Instantiating a file-backed instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::File;
///
/// let db = Surreal::new::<File>("temp.db").await?;
/// # Ok(())
/// # }
/// ```
///
/// Instantiating a file-backed strict instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::opt::Strict;
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::File;
///
/// let db = Surreal::new::<File>(("temp.db", Strict)).await?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "kv-rocksdb")]
#[cfg_attr(docsrs, doc(cfg(feature = "kv-rocksdb")))]
#[derive(Debug)]
pub struct File;
/// RocksDB database
///
/// # Examples
///
/// Instantiating a RocksDB-backed instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::RocksDb;
///
/// let db = Surreal::new::<RocksDb>("temp.db").await?;
/// # Ok(())
/// # }
/// ```
///
/// Instantiating a RocksDB-backed strict instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::opt::Strict;
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::RocksDb;
///
/// let db = Surreal::new::<RocksDb>(("temp.db", Strict)).await?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "kv-rocksdb")]
#[cfg_attr(docsrs, doc(cfg(feature = "kv-rocksdb")))]
#[derive(Debug)]
pub struct RocksDb;
/// IndxDB database
///
/// # Examples
///
/// Instantiating a IndxDB-backed instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::IndxDb;
///
/// let db = Surreal::new::<IndxDb>("MyDatabase").await?;
/// # Ok(())
/// # }
/// ```
///
/// Instantiating an IndxDB-backed strict instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::opt::Strict;
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::IndxDb;
///
/// let db = Surreal::new::<IndxDb>(("MyDatabase", Strict)).await?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "kv-indxdb")]
#[cfg_attr(docsrs, doc(cfg(feature = "kv-indxdb")))]
#[derive(Debug)]
pub struct IndxDb;
/// TiKV database
///
/// # Examples
///
/// Instantiating a TiKV instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::TiKv;
///
/// let db = Surreal::new::<TiKv>("localhost:2379").await?;
/// # Ok(())
/// # }
/// ```
///
/// Instantiating a TiKV strict instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::opt::Strict;
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::TiKv;
///
/// let db = Surreal::new::<TiKv>(("localhost:2379", Strict)).await?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "kv-tikv")]
#[cfg_attr(docsrs, doc(cfg(feature = "kv-tikv")))]
#[derive(Debug)]
pub struct TiKv;
/// FoundationDB database
///
/// # Examples
///
/// Instantiating a FoundationDB-backed instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::opt::Strict;
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::FDb;
///
/// let db = Surreal::new::<FDb>("fdb.cluster").await?;
/// # Ok(())
/// # }
/// ```
///
/// Instantiating a FoundationDB-backed strict instance
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::opt::Strict;
/// use surrealdb::Surreal;
/// use surrealdb::engines::local::FDb;
///
/// let db = Surreal::new::<FDb>(("fdb.cluster", Strict)).await?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "kv-fdb")]
#[cfg_attr(docsrs, doc(cfg(feature = "kv-fdb")))]
#[derive(Debug)]
pub struct FDb;
/// An embedded database
///
/// Authentication methods (`signup`, `signin`, `authentication` and `invalidate`) are not availabe
/// on `Db`
#[derive(Debug, Clone)]
pub struct Db {
pub(crate) method: crate::api::conn::Method,
}
impl Surreal<Db> {
/// Connects to a specific database endpoint, saving the connection on the static client
pub fn connect<P>(
&'static self,
address: impl IntoEndpoint<P, Client = Db>,
) -> Connect<Db, ()> {
Connect {
router: Some(&self.router),
address: address.into_endpoint(),
capacity: 0,
client: PhantomData,
response_type: PhantomData,
}
}
}
fn process(responses: Vec<Response>) -> Result<QueryResponse> {
let mut map = IndexMap::with_capacity(responses.len());
for (index, response) in responses.into_iter().enumerate() {
match response.result {
Ok(value) => match value {
Value::Array(Array(array)) => map.insert(index, Ok(array)),
Value::None | Value::Null => map.insert(index, Ok(vec![])),
value => map.insert(index, Ok(vec![value])),
},
Err(error) => map.insert(index, Err(error.into())),
};
}
Ok(QueryResponse(map))
}
async fn take(one: bool, responses: Vec<Response>) -> Result<Value> {
if let Some(result) = process(responses)?.0.remove(&0) {
let mut vec = result?;
match one {
true => match vec.pop() {
Some(Value::Array(Array(mut vec))) => {
if let [value] = &mut vec[..] {
return Ok(mem::take(value));
}
}
Some(Value::None | Value::Null) | None => {}
Some(value) => {
return Ok(value);
}
},
false => {
return Ok(Value::Array(Array(vec)));
}
}
}
match one {
true => Ok(Value::None),
false => Ok(Value::Array(Array(vec![]))),
}
}
async fn router(
(_, method, param): (i64, Method, Param),
kvs: &Datastore,
session: &mut Session,
vars: &mut BTreeMap<String, Value>,
strict: bool,
) -> Result<DbResponse> {
let mut params = param.other;
match method {
Method::Use => {
let (ns, db) = match &mut params[..] {
[Value::Strand(Strand(ns)), Value::Strand(Strand(db))] => {
(mem::take(ns), mem::take(db))
}
_ => unreachable!(),
};
session.ns = Some(ns);
session.db = Some(db);
Ok(DbResponse::Other(Value::None))
}
Method::Signin | Method::Signup | Method::Authenticate | Method::Invalidate => {
unreachable!()
}
Method::Create => {
let statement = create_statement(&mut params);
let query = Query(Statements(vec![Statement::Create(statement)]));
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
let value = take(true, response).await?;
Ok(DbResponse::Other(value))
}
Method::Update => {
let (one, statement) = update_statement(&mut params);
let query = Query(Statements(vec![Statement::Update(statement)]));
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
let value = take(one, response).await?;
Ok(DbResponse::Other(value))
}
Method::Patch => {
let (one, statement) = patch_statement(&mut params);
let query = Query(Statements(vec![Statement::Update(statement)]));
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
let value = take(one, response).await?;
Ok(DbResponse::Other(value))
}
Method::Merge => {
let (one, statement) = merge_statement(&mut params);
let query = Query(Statements(vec![Statement::Update(statement)]));
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
let value = take(one, response).await?;
Ok(DbResponse::Other(value))
}
Method::Select => {
let (one, statement) = select_statement(&mut params);
let query = Query(Statements(vec![Statement::Select(statement)]));
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
let value = take(one, response).await?;
Ok(DbResponse::Other(value))
}
Method::Delete => {
let statement = delete_statement(&mut params);
let query = Query(Statements(vec![Statement::Delete(statement)]));
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
let value = take(true, response).await?;
Ok(DbResponse::Other(value))
}
Method::Query => {
let response = match param.query {
Some((query, mut bindings)) => {
let mut vars = vars.clone();
vars.append(&mut bindings);
kvs.process(query, &*session, Some(vars), strict).await?
}
None => unreachable!(),
};
let response = process(response)?;
Ok(DbResponse::Query(response))
}
#[cfg(target_arch = "wasm32")]
Method::Export | Method::Import => unreachable!(),
#[cfg(not(target_arch = "wasm32"))]
Method::Export => {
let (tx, rx) = channel::new::<Vec<u8>>(1);
let ns = session.ns.clone().unwrap_or_default();
let db = session.db.clone().unwrap_or_default();
let (mut writer, mut reader) = io::duplex(10_240);
tokio::spawn(async move {
while let Ok(value) = rx.recv().await {
if let Err(error) = writer.write_all(&value).await {
error!(target: LOG, "{error}");
}
}
});
if let Err(error) = kvs.export(ns, db, tx).await {
error!(target: LOG, "{error}");
}
let path = param.file.expect("file to export into");
let mut file = match OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&path)
.await
{
Ok(path) => path,
Err(error) => {
return Err(Error::FileOpen {
path,
error,
}
.into());
}
};
if let Err(error) = io::copy(&mut reader, &mut file).await {
return Err(Error::FileRead {
path,
error,
}
.into());
};
Ok(DbResponse::Other(Value::None))
}
#[cfg(not(target_arch = "wasm32"))]
Method::Import => {
let path = param.file.expect("file to import from");
let mut file = match OpenOptions::new().read(true).open(&path).await {
Ok(path) => path,
Err(error) => {
return Err(Error::FileOpen {
path,
error,
}
.into());
}
};
let mut statements = String::new();
if let Err(error) = file.read_to_string(&mut statements).await {
return Err(Error::FileRead {
path,
error,
}
.into());
}
let responses = kvs.execute(&statements, &*session, Some(vars.clone()), strict).await?;
for response in responses {
response.result?;
}
Ok(DbResponse::Other(Value::None))
}
Method::Health => Ok(DbResponse::Other(Value::None)),
Method::Version => Ok(DbResponse::Other(crate::env::VERSION.into())),
Method::Set => {
let (key, value) = match &mut params[..2] {
[Value::Strand(Strand(key)), value] => (mem::take(key), mem::take(value)),
_ => unreachable!(),
};
vars.insert(key, value);
Ok(DbResponse::Other(Value::None))
}
Method::Unset => {
if let [Value::Strand(Strand(key))] = &params[..1] {
vars.remove(key);
}
Ok(DbResponse::Other(Value::None))
}
Method::Live => {
let table = match &mut params[..] {
[value] => mem::take(value),
_ => unreachable!(),
};
let mut vars = BTreeMap::new();
vars.insert("table".to_owned(), table);
let response = kvs
.execute("LIVE SELECT * FROM type::table($table)", &*session, Some(vars), strict)
.await?;
let value = take(true, response).await?;
Ok(DbResponse::Other(value))
}
Method::Kill => {
let id = match &mut params[..] {
[value] => mem::take(value),
_ => unreachable!(),
};
let mut vars = BTreeMap::new();
vars.insert("id".to_owned(), id);
let response =
kvs.execute("KILL type::string($id)", &*session, Some(vars), strict).await?;
let value = take(true, response).await?;
Ok(DbResponse::Other(value))
}
}
}

View file

@ -0,0 +1,154 @@
use crate::api::conn::Connection;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
use crate::api::engines::local::Db;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
use crate::api::ExtraFeatures;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use crate::dbs::Session;
use crate::kvs::Datastore;
use flume::Receiver;
use flume::Sender;
use futures::StreamExt;
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
impl crate::api::Connection for Db {}
impl Connection for Db {
fn new(method: Method) -> Self {
Self {
method,
}
}
fn connect(
address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
let (route_tx, route_rx) = match capacity {
0 => flume::unbounded(),
capacity => flume::bounded(capacity),
};
let (conn_tx, conn_rx) = flume::bounded(1);
router(address, conn_tx, route_rx);
conn_rx.into_recv_async().await??;
let mut features = HashSet::new();
features.insert(ExtraFeatures::Backup);
Ok(Surreal {
router: OnceCell::with_value(Arc::new(Router {
features,
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
})),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
let (sender, receiver) = flume::bounded(1);
let route = Route {
request: (0, self.method, param),
response: sender,
};
router.sender.send_async(Some(route)).await?;
Ok(receiver)
})
}
fn recv<R>(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let response = rx.into_recv_async().await?;
match response? {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
Box::pin(async move {
let response = rx.into_recv_async().await?;
match response? {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}
pub(crate) fn router(
address: Endpoint,
conn_tx: Sender<Result<()>>,
route_rx: Receiver<Option<Route>>,
) {
tokio::spawn(async move {
let url = address.endpoint;
let path = match url.scheme() {
"mem" => "memory",
_ => url.as_str(),
};
let kvs = match Datastore::new(path).await {
Ok(kvs) => {
let _ = conn_tx.into_send_async(Ok(())).await;
kvs
}
Err(error) => {
let _ = conn_tx.into_send_async(Err(error.into())).await;
return;
}
};
let mut session = Session::for_kv();
let mut vars = BTreeMap::new();
let mut stream = route_rx.into_stream();
while let Some(Some(route)) = stream.next().await {
match super::router(route.request, &kvs, &mut session, &mut vars, address.strict).await
{
Ok(value) => {
let _ = route.response.into_send_async(Ok(value)).await;
}
Err(error) => {
let _ = route.response.into_send_async(Err(error)).await;
}
}
}
});
}

View file

@ -0,0 +1,156 @@
use super::LOG;
use crate::api::conn::Connection;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
use crate::api::engines::local::Db;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use crate::dbs::Session;
use crate::kvs::Datastore;
use flume::Receiver;
use flume::Sender;
use futures::StreamExt;
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
use wasm_bindgen_futures::spawn_local;
impl crate::api::Connection for Db {}
impl Connection for Db {
fn new(method: Method) -> Self {
Self {
method,
}
}
fn connect(
address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
let (route_tx, route_rx) = match capacity {
0 => flume::unbounded(),
capacity => flume::bounded(capacity),
};
let (conn_tx, conn_rx) = flume::bounded(1);
router(address, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
Ok(Surreal {
router: OnceCell::with_value(Arc::new(Router {
features: HashSet::new(),
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
})),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
let (sender, receiver) = flume::bounded(1);
let route = Route {
request: (0, self.method, param),
response: sender,
};
router.sender.send_async(Some(route)).await?;
Ok(receiver)
})
}
fn recv<R>(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let response = rx.into_recv_async().await?;
trace!(target: LOG, "Response {response:?}");
match response? {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
Box::pin(async move {
let response = rx.into_recv_async().await?;
trace!(target: LOG, "Response {response:?}");
match response? {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}
pub(crate) fn router(
address: Endpoint,
conn_tx: Sender<Result<()>>,
route_rx: Receiver<Option<Route>>,
) {
spawn_local(async move {
let url = address.endpoint;
let path = match url.scheme() {
"mem" => "memory",
_ => url.as_str(),
};
let kvs = match Datastore::new(path).await {
Ok(kvs) => {
let _ = conn_tx.into_send_async(Ok(())).await;
kvs
}
Err(error) => {
let _ = conn_tx.into_send_async(Err(error.into())).await;
return;
}
};
let mut session = Session::for_kv();
let mut vars = BTreeMap::new();
let mut stream = route_rx.into_stream();
while let Some(Some(route)) = stream.next().await {
match super::router(route.request, &kvs, &mut session, &mut vars, address.strict).await
{
Ok(value) => {
let _ = route.response.into_send_async(Ok(value)).await;
}
Err(error) => {
let _ = route.response.into_send_async(Err(error)).await;
}
}
}
});
}

133
lib/src/api/engines/mod.rs Normal file
View file

@ -0,0 +1,133 @@
//! Different embedded and remote database engines
pub mod any;
#[cfg(any(
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-fdb",
feature = "kv-indxdb",
))]
pub mod local;
#[cfg(any(feature = "protocol-http", feature = "protocol-ws"))]
pub mod remote;
use crate::sql::statements::CreateStatement;
use crate::sql::statements::DeleteStatement;
use crate::sql::statements::SelectStatement;
use crate::sql::statements::UpdateStatement;
use crate::sql::Array;
use crate::sql::Data;
use crate::sql::Field;
use crate::sql::Fields;
use crate::sql::Output;
use crate::sql::Value;
use crate::sql::Values;
use std::mem;
#[allow(dead_code)] // used by the the embedded database and `http`
fn split_params(params: &mut [Value]) -> (bool, Values, Value) {
let (what, data) = match params {
[what] => (mem::take(what), Value::None),
[what, data] => (mem::take(what), mem::take(data)),
_ => unreachable!(),
};
let one = what.is_thing();
let what = match what {
Value::Array(Array(vec)) => Values(vec),
value => Values(vec![value]),
};
(one, what, data)
}
#[allow(dead_code)] // used by the the embedded database and `http`
fn create_statement(params: &mut [Value]) -> CreateStatement {
let (_, what, data) = split_params(params);
let data = match data {
Value::None | Value::Null => None,
value => Some(Data::ContentExpression(value)),
};
CreateStatement {
what,
data,
output: Some(Output::After),
..Default::default()
}
}
#[allow(dead_code)] // used by the the embedded database and `http`
fn update_statement(params: &mut [Value]) -> (bool, UpdateStatement) {
let (one, what, data) = split_params(params);
let data = match data {
Value::None | Value::Null => None,
value => Some(Data::ContentExpression(value)),
};
(
one,
UpdateStatement {
what,
data,
output: Some(Output::After),
..Default::default()
},
)
}
#[allow(dead_code)] // used by the the embedded database and `http`
fn patch_statement(params: &mut [Value]) -> (bool, UpdateStatement) {
let (one, what, data) = split_params(params);
let data = match data {
Value::None | Value::Null => None,
value => Some(Data::PatchExpression(value)),
};
(
one,
UpdateStatement {
what,
data,
output: Some(Output::Diff),
..Default::default()
},
)
}
#[allow(dead_code)] // used by the the embedded database and `http`
fn merge_statement(params: &mut [Value]) -> (bool, UpdateStatement) {
let (one, what, data) = split_params(params);
let data = match data {
Value::None | Value::Null => None,
value => Some(Data::MergeExpression(value)),
};
(
one,
UpdateStatement {
what,
data,
output: Some(Output::After),
..Default::default()
},
)
}
#[allow(dead_code)] // used by the the embedded database and `http`
fn select_statement(params: &mut [Value]) -> (bool, SelectStatement) {
let (one, what, _) = split_params(params);
(
one,
SelectStatement {
what,
expr: Fields(vec![Field::All]),
..Default::default()
},
)
}
#[allow(dead_code)] // used by the the embedded database and `http`
fn delete_statement(params: &mut [Value]) -> DeleteStatement {
let (_, what, _) = split_params(params);
DeleteStatement {
what,
output: Some(Output::None),
..Default::default()
}
}

View file

@ -0,0 +1,578 @@
//! HTTP engine
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod native;
#[cfg(target_arch = "wasm32")]
pub(crate) mod wasm;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::engines::create_statement;
use crate::api::engines::delete_statement;
use crate::api::engines::merge_statement;
use crate::api::engines::patch_statement;
use crate::api::engines::remote::Status;
use crate::api::engines::select_statement;
use crate::api::engines::update_statement;
use crate::api::err::Error;
use crate::api::method::query::QueryResult;
use crate::api::opt::from_json;
use crate::api::opt::from_value;
use crate::api::Connect;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use crate::opt::IntoEndpoint;
use crate::sql::Array;
use crate::sql::Strand;
use crate::sql::Value;
#[cfg(not(target_arch = "wasm32"))]
use futures::TryStreamExt;
use indexmap::IndexMap;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderValue;
#[cfg(not(target_arch = "wasm32"))]
use reqwest::header::ACCEPT;
#[cfg(not(target_arch = "wasm32"))]
use reqwest::header::CONTENT_TYPE;
use reqwest::RequestBuilder;
use serde::Deserialize;
use serde::Serialize;
use std::marker::PhantomData;
use std::mem;
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
#[cfg(not(target_arch = "wasm32"))]
use tokio::fs::OpenOptions;
#[cfg(not(target_arch = "wasm32"))]
use tokio::io;
#[cfg(not(target_arch = "wasm32"))]
use tokio::io::AsyncReadExt;
#[cfg(not(target_arch = "wasm32"))]
use tokio_util::compat::FuturesAsyncReadCompatExt;
use url::Url;
const SQL_PATH: &str = "sql";
const LOG: &str = "surrealdb::engines::remote::http";
/// The HTTP scheme used to connect to `http://` endpoints
#[derive(Debug)]
pub struct Http;
/// The HTTPS scheme used to connect to `https://` endpoints
#[derive(Debug)]
pub struct Https;
/// An HTTP client for communicating with the server via HTTP
#[derive(Debug, Clone)]
pub struct Client {
method: Method,
}
impl Surreal<Client> {
/// Connects to a specific database endpoint, saving the connection on the static client
///
/// # Examples
///
/// ```no_run
/// use surrealdb::Surreal;
/// use surrealdb::engines::remote::http::Client;
/// use surrealdb::engines::remote::http::Http;
///
/// static DB: Surreal<Client> = Surreal::init();
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// DB.connect::<Http>("localhost:8000").await?;
/// # Ok(())
/// # }
/// ```
pub fn connect<P>(
&'static self,
address: impl IntoEndpoint<P, Client = Client>,
) -> Connect<Client, ()> {
Connect {
router: Some(&self.router),
address: address.into_endpoint(),
capacity: 0,
client: PhantomData,
response_type: PhantomData,
}
}
}
enum Auth {
Basic {
user: String,
pass: String,
},
Bearer {
token: String,
},
}
trait Authenticate {
fn auth(self, auth: &Option<Auth>) -> Self;
}
impl Authenticate for RequestBuilder {
fn auth(self, auth: &Option<Auth>) -> Self {
match auth {
Some(Auth::Basic {
user,
pass,
}) => self.basic_auth(user, Some(pass)),
Some(Auth::Bearer {
token,
}) => self.bearer_auth(token),
None => self,
}
}
}
#[derive(Debug, Deserialize)]
struct HttpQueryResponse {
status: Status,
result: Option<serde_json::Value>,
detail: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Root {
user: String,
pass: String,
}
#[derive(Debug, Deserialize)]
struct AuthResponse {
token: Option<String>,
}
async fn submit_auth(request: RequestBuilder) -> Result<Value> {
let response = request.send().await?.error_for_status()?;
let text = response.text().await?;
info!(target: LOG, "Response {text}");
let response: AuthResponse = match serde_json::from_str(&text) {
Ok(response) => response,
Err(error) => {
return Err(Error::FromJsonString {
string: text,
error: error.to_string(),
}
.into());
}
};
Ok(response.token.filter(|token| token != "NONE").into())
}
async fn query(request: RequestBuilder) -> Result<QueryResponse> {
info!(target: LOG, "{request:?}");
let response = request.send().await?.error_for_status()?;
let text = response.text().await?;
info!(target: LOG, "Response {text}");
let responses: Vec<HttpQueryResponse> = match serde_json::from_str(&text) {
Ok(vec) => vec,
Err(error) => {
return Err(Error::FromJsonString {
string: text,
error: error.to_string(),
}
.into());
}
};
let mut map = IndexMap::<usize, QueryResult>::with_capacity(responses.len());
for (index, response) in responses.into_iter().enumerate() {
match response.status {
Status::Ok => {
if let Some(value) = response.result {
match from_json(value) {
Value::Array(Array(array)) => map.insert(index, Ok(array).into()),
Value::None | Value::Null => map.insert(index, Ok(vec![]).into()),
value => map.insert(index, Ok(vec![value]).into()),
};
}
}
Status::Err => {
if let Some(error) = response.detail {
map.insert(index, Err(Error::Query(error).into()));
}
}
}
}
Ok(QueryResponse(map))
}
async fn take(one: bool, request: RequestBuilder) -> Result<Value> {
if let Some(result) = query(request).await?.0.remove(&0) {
let mut vec = result?;
match one {
true => match vec.pop() {
Some(Value::Array(Array(mut vec))) => {
if let [value] = &mut vec[..] {
return Ok(mem::take(value));
}
}
Some(Value::None | Value::Null) | None => {}
Some(value) => {
return Ok(value);
}
},
false => {
return Ok(Value::Array(Array(vec)));
}
}
}
match one {
true => Ok(Value::None),
false => Ok(Value::Array(Array(vec![]))),
}
}
#[cfg(not(target_arch = "wasm32"))]
async fn export(request: RequestBuilder, path: PathBuf) -> Result<Value> {
let mut file =
match OpenOptions::new().write(true).create(true).truncate(true).open(&path).await {
Ok(path) => path,
Err(error) => {
return Err(Error::FileOpen {
path,
error,
}
.into());
}
};
let mut response = request
.send()
.await?
.error_for_status()?
.bytes_stream()
.map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e))
.into_async_read()
.compat();
if let Err(error) = io::copy(&mut response, &mut file).await {
return Err(Error::FileRead {
path,
error,
}
.into());
}
Ok(Value::None)
}
#[cfg(not(target_arch = "wasm32"))]
async fn import(request: RequestBuilder, path: PathBuf) -> Result<Value> {
let mut file = match OpenOptions::new().read(true).open(&path).await {
Ok(path) => path,
Err(error) => {
return Err(Error::FileOpen {
path,
error,
}
.into());
}
};
let mut contents = vec![];
if let Err(error) = file.read_to_end(&mut contents).await {
return Err(Error::FileRead {
path,
error,
}
.into());
}
// ideally we should pass `file` directly into the body
// but currently that results in
// "HTTP status client error (405 Method Not Allowed) for url"
request.body(contents).send().await?.error_for_status()?;
Ok(Value::None)
}
async fn version(request: RequestBuilder) -> Result<Value> {
let response = request.send().await?.error_for_status()?;
let version = response.text().await?;
Ok(version.into())
}
pub(crate) async fn health(request: RequestBuilder) -> Result<Value> {
request.send().await?.error_for_status()?;
Ok(Value::None)
}
async fn router(
(_, method, param): (i64, Method, Param),
base_url: &Url,
client: &reqwest::Client,
headers: &mut HeaderMap,
vars: &mut IndexMap<String, String>,
auth: &mut Option<Auth>,
) -> Result<DbResponse> {
let mut params = match param.query {
Some((query, bindings)) => {
vec![query.to_string().into(), bindings.into()]
}
None => param.other,
};
match method {
Method::Use => {
let path = base_url.join(SQL_PATH)?;
let (ns, db) = match &mut params[..] {
[Value::Strand(Strand(ns)), Value::Strand(Strand(db))] => {
(mem::take(ns), mem::take(db))
}
_ => unreachable!(),
};
let ns = match HeaderValue::try_from(&ns) {
Ok(ns) => ns,
Err(_) => {
return Err(Error::InvalidNsName(ns).into());
}
};
let db = match HeaderValue::try_from(&db) {
Ok(db) => db,
Err(_) => {
return Err(Error::InvalidDbName(db).into());
}
};
let request = client
.post(path)
.headers(headers.clone())
.header("NS", &ns)
.header("DB", &db)
.auth(&auth)
.body("RETURN true");
take(true, request).await?;
headers.insert("NS", ns);
headers.insert("DB", db);
Ok(DbResponse::Other(Value::None))
}
Method::Signin => {
let path = base_url.join(Method::Signin.as_str())?;
let credentials = match &mut params[..] {
[credentials] => match serde_json::to_string(credentials) {
Ok(json) => json,
Err(error) => {
return Err(Error::ToJsonString {
value: mem::take(credentials),
error: error.to_string(),
}
.into());
}
},
_ => unreachable!(),
};
let request = client.post(path).headers(headers.clone()).auth(&auth).body(credentials);
let value = submit_auth(request).await?;
if let [credentials] = &mut params[..] {
if let Ok(Root {
user,
pass,
}) = from_value(mem::take(credentials))
{
*auth = Some(Auth::Basic {
user,
pass,
});
} else {
*auth = Some(Auth::Bearer {
token: value.to_strand().as_string(),
});
}
}
Ok(DbResponse::Other(value))
}
Method::Signup => {
let path = base_url.join(Method::Signup.as_str())?;
let credentials = match &mut params[..] {
[credentials] => match serde_json::to_string(credentials) {
Ok(json) => json,
Err(error) => {
return Err(Error::ToJsonString {
value: mem::take(credentials),
error: error.to_string(),
}
.into());
}
},
_ => unreachable!(),
};
let request = client.post(path).headers(headers.clone()).auth(&auth).body(credentials);
let value = submit_auth(request).await?;
Ok(DbResponse::Other(value))
}
Method::Authenticate => {
let path = base_url.join(SQL_PATH)?;
let token = match &mut params[..1] {
[Value::Strand(Strand(token))] => mem::take(token),
_ => unreachable!(),
};
let request =
client.post(path).headers(headers.clone()).bearer_auth(&token).body("RETURN true");
take(true, request).await?;
*auth = Some(Auth::Bearer {
token,
});
Ok(DbResponse::Other(Value::None))
}
Method::Invalidate => {
*auth = None;
Ok(DbResponse::Other(Value::None))
}
Method::Create => {
let path = base_url.join(SQL_PATH)?;
let statement = create_statement(&mut params);
let request =
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
let value = take(true, request).await?;
Ok(DbResponse::Other(value))
}
Method::Update => {
let path = base_url.join(SQL_PATH)?;
let (one, statement) = update_statement(&mut params);
let request =
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
let value = take(one, request).await?;
Ok(DbResponse::Other(value))
}
Method::Patch => {
let path = base_url.join(SQL_PATH)?;
let (one, statement) = patch_statement(&mut params);
let request =
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
let value = take(one, request).await?;
Ok(DbResponse::Other(value))
}
Method::Merge => {
let path = base_url.join(SQL_PATH)?;
let (one, statement) = merge_statement(&mut params);
let request =
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
let value = take(one, request).await?;
Ok(DbResponse::Other(value))
}
Method::Select => {
let path = base_url.join(SQL_PATH)?;
let (one, statement) = select_statement(&mut params);
let request =
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
let value = take(one, request).await?;
Ok(DbResponse::Other(value))
}
Method::Delete => {
let path = base_url.join(SQL_PATH)?;
let statement = delete_statement(&mut params);
let request =
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
let value = take(true, request).await?;
Ok(DbResponse::Other(value))
}
Method::Query => {
let path = base_url.join(SQL_PATH)?;
let mut request = client.post(path).headers(headers.clone()).query(&vars).auth(&auth);
match &mut params[..] {
[Value::Strand(Strand(statements))] => {
request = request.body(mem::take(statements));
}
[Value::Strand(Strand(statements)), Value::Object(bindings)] => {
let bindings: Vec<_> =
bindings.iter().map(|(key, value)| (key, value.to_string())).collect();
request = request.query(&bindings).body(mem::take(statements));
}
_ => unreachable!(),
}
let values = query(request).await?;
Ok(DbResponse::Query(values))
}
#[cfg(target_arch = "wasm32")]
Method::Export | Method::Import => unreachable!(),
#[cfg(not(target_arch = "wasm32"))]
Method::Export => {
let path = base_url.join(Method::Export.as_str())?;
let file = param.file.expect("file to export into");
let request = client
.get(path)
.headers(headers.clone())
.auth(&auth)
.header(ACCEPT, "application/octet-stream");
let value = export(request, file).await?;
Ok(DbResponse::Other(value))
}
#[cfg(not(target_arch = "wasm32"))]
Method::Import => {
let path = base_url.join(Method::Import.as_str())?;
let file = param.file.expect("file to import from");
let request = client
.post(path)
.headers(headers.clone())
.auth(&auth)
.header(CONTENT_TYPE, "application/octet-stream");
let value = import(request, file).await?;
Ok(DbResponse::Other(value))
}
Method::Health => {
let path = base_url.join(Method::Health.as_str())?;
let request = client.get(path);
let value = health(request).await?;
Ok(DbResponse::Other(value))
}
Method::Version => {
let path = base_url.join(method.as_str())?;
let request = client.get(path);
let value = version(request).await?;
Ok(DbResponse::Other(value))
}
Method::Set => {
let path = base_url.join(SQL_PATH)?;
let (key, value) = match &mut params[..2] {
[Value::Strand(Strand(key)), value] => (mem::take(key), value.to_string()),
_ => unreachable!(),
};
let request = client
.post(path)
.headers(headers.clone())
.auth(&auth)
.query(&[(key.as_str(), value.as_str())])
.body(format!("RETURN ${key}"));
take(true, request).await?;
vars.insert(key, value);
Ok(DbResponse::Other(Value::None))
}
Method::Unset => {
if let [Value::Strand(Strand(key))] = &params[..1] {
vars.remove(key);
}
Ok(DbResponse::Other(Value::None))
}
Method::Live => {
let path = base_url.join(SQL_PATH)?;
let table = match &params[..] {
[table] => table.to_string(),
_ => unreachable!(),
};
let request = client
.post(path)
.headers(headers.clone())
.auth(&auth)
.query(&[("table", table)])
.body("LIVE SELECT * FROM type::table($table)");
let value = take(true, request).await?;
Ok(DbResponse::Other(value))
}
Method::Kill => {
let path = base_url.join(SQL_PATH)?;
let id = match &params[..] {
[id] => id.to_string(),
_ => unreachable!(),
};
let request = client
.post(path)
.headers(headers.clone())
.auth(&auth)
.query(&[("id", id)])
.body("KILL type::string($id)");
let value = take(true, request).await?;
Ok(DbResponse::Other(value))
}
}
}

View file

@ -0,0 +1,167 @@
use super::Client;
use super::LOG;
use crate::api::conn::Connection;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use crate::api::opt::Tls;
use crate::api::ExtraFeatures;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use flume::Receiver;
use futures::StreamExt;
use indexmap::IndexMap;
use once_cell::sync::OnceCell;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderValue;
use reqwest::header::ACCEPT;
use reqwest::ClientBuilder;
use serde::de::DeserializeOwned;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
use url::Url;
impl crate::api::Connection for Client {}
impl Connection for Client {
fn new(method: Method) -> Self {
Self {
method,
}
}
fn connect(
address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
#[allow(unused_mut)]
let mut builder = ClientBuilder::new().default_headers(headers);
#[cfg(any(feature = "native-tls", feature = "rustls"))]
if let Some(tls) = address.tls_config {
builder = match tls {
#[cfg(feature = "native-tls")]
Tls::Native(config) => builder.use_preconfigured_tls(config),
#[cfg(feature = "rustls")]
Tls::Rust(config) => builder.use_preconfigured_tls(config),
};
}
let client = builder.build()?;
let base_url = address.endpoint;
super::health(client.get(base_url.join(Method::Health.as_str())?)).await?;
let (route_tx, route_rx) = match capacity {
0 => flume::unbounded(),
capacity => flume::bounded(capacity),
};
router(base_url, client, route_rx);
let mut features = HashSet::new();
features.insert(ExtraFeatures::Auth);
features.insert(ExtraFeatures::Backup);
Ok(Surreal {
router: OnceCell::with_value(Arc::new(Router {
features,
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
})),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
let (sender, receiver) = flume::bounded(1);
let route = Route {
request: (0, self.method, param),
response: sender,
};
router.sender.send_async(Some(route)).await?;
Ok(receiver)
})
}
fn recv<R>(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let response = rx.into_recv_async().await?;
trace!(target: LOG, "Response {response:?}");
match response? {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
Box::pin(async move {
let response = rx.into_recv_async().await?;
trace!(target: LOG, "Response {response:?}");
match response? {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}
pub(crate) fn router(base_url: Url, client: reqwest::Client, route_rx: Receiver<Option<Route>>) {
tokio::spawn(async move {
let mut headers = HeaderMap::new();
let mut vars = IndexMap::new();
let mut auth = None;
let mut stream = route_rx.into_stream();
while let Some(Some(route)) = stream.next().await {
match super::router(
route.request,
&base_url,
&client,
&mut headers,
&mut vars,
&mut auth,
)
.await
{
Ok(value) => {
let _ = route.response.into_send_async(Ok(value)).await;
}
Err(error) => {
let _ = route.response.into_send_async(Err(error)).await;
}
}
}
});
}

View file

@ -0,0 +1,178 @@
use super::Client;
use super::LOG;
use crate::api::conn::Connection;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
use crate::api::ExtraFeatures;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use flume::Receiver;
use flume::Sender;
use futures::StreamExt;
use indexmap::IndexMap;
use once_cell::sync::OnceCell;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderValue;
use reqwest::header::ACCEPT;
use reqwest::ClientBuilder;
use serde::de::DeserializeOwned;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
use url::Url;
use wasm_bindgen_futures::spawn_local;
impl crate::api::Connection for Client {}
impl Connection for Client {
fn new(method: Method) -> Self {
Self {
method,
}
}
fn connect(
address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
let (route_tx, route_rx) = match capacity {
0 => flume::unbounded(),
capacity => flume::bounded(capacity),
};
let (conn_tx, conn_rx) = flume::bounded(1);
router(address, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
let mut features = HashSet::new();
features.insert(ExtraFeatures::Auth);
Ok(Surreal {
router: OnceCell::with_value(Arc::new(Router {
features,
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
})),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
let (sender, receiver) = flume::bounded(1);
trace!(target: LOG, "{param:?}");
let route = Route {
request: (0, self.method, param),
response: sender,
};
router.sender.send_async(Some(route)).await?;
Ok(receiver)
})
}
fn recv<R>(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let response = rx.into_recv_async().await?;
trace!(target: LOG, "Response {response:?}");
match response? {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
Box::pin(async move {
let response = rx.into_recv_async().await?;
trace!(target: LOG, "Response {response:?}");
match response? {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}
async fn client(base_url: &Url) -> Result<reqwest::Client> {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
let builder = ClientBuilder::new().default_headers(headers);
let client = builder.build()?;
let health = base_url.join(Method::Health.as_str())?;
super::health(client.get(health)).await?;
Ok(client)
}
pub(crate) fn router(
address: Endpoint,
conn_tx: Sender<Result<()>>,
route_rx: Receiver<Option<Route>>,
) {
spawn_local(async move {
let base_url = address.endpoint;
let client = match client(&base_url).await {
Ok(client) => {
let _ = conn_tx.into_send_async(Ok(())).await;
client
}
Err(error) => {
let _ = conn_tx.into_send_async(Err(error.into())).await;
return;
}
};
let mut headers = HeaderMap::new();
let mut vars = IndexMap::new();
let mut auth = None;
let mut stream = route_rx.into_stream();
while let Some(Some(route)) = stream.next().await {
match super::router(
route.request,
&base_url,
&client,
&mut headers,
&mut vars,
&mut auth,
)
.await
{
Ok(value) => {
let _ = route.response.into_send_async(Ok(value)).await;
}
Err(error) => {
let _ = route.response.into_send_async(Err(error)).await;
}
}
}
});
}

View file

@ -0,0 +1,18 @@
//! Protocols for communicating with the server
use serde::Deserialize;
#[cfg(feature = "protocol-http")]
#[cfg_attr(docsrs, doc(cfg(feature = "protocol-http")))]
pub mod http;
#[cfg(feature = "protocol-ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "protocol-ws")))]
pub mod ws;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub(crate) enum Status {
Ok,
Err,
}

View file

@ -0,0 +1,170 @@
//! WebSocket engine
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod native;
#[cfg(target_arch = "wasm32")]
pub(crate) mod wasm;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::engines::remote::Status;
use crate::api::err::Error;
use crate::api::Connect;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use crate::opt::IntoEndpoint;
use crate::sql::Array;
use crate::sql::Value;
use serde::Deserialize;
use std::marker::PhantomData;
use std::mem;
use std::time::Duration;
pub(crate) const PATH: &str = "rpc";
const PING_INTERVAL: Duration = Duration::from_secs(5);
const PING_METHOD: &str = "ping";
const LOG: &str = "surrealdb::engines::remote::ws";
/// The WS scheme used to connect to `ws://` endpoints
#[derive(Debug)]
pub struct Ws;
/// The WSS scheme used to connect to `wss://` endpoints
#[derive(Debug)]
pub struct Wss;
/// A WebSocket client for communicating with the server via WebSockets
#[derive(Debug, Clone)]
pub struct Client {
pub(crate) id: i64,
method: Method,
}
impl Surreal<Client> {
/// Connects to a specific database endpoint, saving the connection on the static client
///
/// # Examples
///
/// ```no_run
/// use surrealdb::Surreal;
/// use surrealdb::engines::remote::ws::Client;
/// use surrealdb::engines::remote::ws::Ws;
///
/// static DB: Surreal<Client> = Surreal::init();
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// DB.connect::<Ws>("localhost:8000").await?;
/// # Ok(())
/// # }
/// ```
pub fn connect<P>(
&'static self,
address: impl IntoEndpoint<P, Client = Client>,
) -> Connect<Client, ()> {
Connect {
router: Some(&self.router),
address: address.into_endpoint(),
capacity: 0,
client: PhantomData,
response_type: PhantomData,
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct Failure {
pub(crate) code: i64,
pub(crate) message: String,
}
impl From<Failure> for Error {
fn from(failure: Failure) -> Self {
match failure.code {
-32600 => Self::InvalidRequest(failure.message),
-32602 => Self::InvalidParams(failure.message),
-32603 => Self::InternalError(failure.message),
-32700 => Self::ParseError(failure.message),
_ => Self::Query(failure.message),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub(crate) enum QueryMethodResponse {
Value(Value),
String(String),
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub(crate) enum SuccessValue {
Query(Vec<(String, Status, QueryMethodResponse)>),
Other(Value),
}
#[derive(Debug, Deserialize)]
pub(crate) enum Content {
#[serde(rename = "result")]
Success(SuccessValue),
#[serde(rename = "error")]
Failure(Failure),
}
impl DbResponse {
fn from((method, content): (Method, Content)) -> Result<Self> {
match content {
Content::Success(SuccessValue::Query(results)) => Ok(DbResponse::Query(QueryResponse(
results
.into_iter()
.map(|(_duration, status, result)| match status {
Status::Ok => match result {
QueryMethodResponse::Value(value) => match value {
Value::Array(Array(values)) => Ok(values),
Value::None | Value::Null => Ok(vec![]),
value => Ok(vec![value]),
},
QueryMethodResponse::String(string) => Ok(vec![string.into()]),
},
Status::Err => match result {
QueryMethodResponse::Value(message) => {
Err(Error::Query(message.to_string()).into())
}
QueryMethodResponse::String(message) => {
Err(Error::Query(message).into())
}
},
})
.enumerate()
.collect(),
))),
Content::Success(SuccessValue::Other(mut value)) => {
if let Method::Create | Method::Delete = method {
if let Value::Array(Array(array)) = &mut value {
match &mut array[..] {
[] => {
value = Value::None;
}
[v] => {
value = mem::take(v);
}
_ => {}
}
}
}
Ok(DbResponse::Other(value))
}
Content::Failure(failure) => Err(Error::from(failure).into()),
}
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct Response {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<Value>,
#[serde(flatten)]
pub(crate) content: Content,
}

View file

@ -0,0 +1,467 @@
use super::LOG;
use super::PATH;
use crate::api::conn::Connection;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
use crate::api::engines::remote::ws::Client;
use crate::api::engines::remote::ws::Response;
use crate::api::engines::remote::ws::PING_INTERVAL;
use crate::api::engines::remote::ws::PING_METHOD;
use crate::api::err::Error;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use crate::api::opt::Tls;
use crate::api::ExtraFeatures;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use crate::sql::Strand;
use crate::sql::Value;
use flume::Receiver;
use futures::stream::SplitSink;
use futures::SinkExt;
use futures::StreamExt;
use futures_concurrency::stream::Merge as _;
use indexmap::IndexMap;
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use std::borrow::BorrowMut;
use std::collections::hash_map::Entry;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::mem;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
use std::time::Instant;
use tokio::net::TcpStream;
use tokio::time;
use tokio::time::MissedTickBehavior;
use tokio_stream::wrappers::IntervalStream;
use tokio_tungstenite::tungstenite::error::Error as WsError;
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::Connector;
use tokio_tungstenite::MaybeTlsStream;
use tokio_tungstenite::WebSocketStream;
use url::Url;
type WsResult<T> = std::result::Result<T, WsError>;
pub(crate) const MAX_MESSAGE_SIZE: usize = 64 << 20; // 64 MiB
pub(crate) const MAX_FRAME_SIZE: usize = 16 << 20; // 16 MiB
pub(crate) enum Either {
Request(Option<Route>),
Response(WsResult<Message>),
Ping,
}
#[cfg(any(feature = "native-tls", feature = "rustls"))]
impl From<Tls> for Connector {
fn from(tls: Tls) -> Self {
match tls {
#[cfg(feature = "native-tls")]
Tls::Native(config) => Self::NativeTls(config),
#[cfg(feature = "rustls")]
Tls::Rust(config) => Self::Rustls(Arc::new(config)),
}
}
}
pub(crate) async fn connect(
url: &Url,
config: Option<WebSocketConfig>,
#[allow(unused_variables)] maybe_connector: Option<Connector>,
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
#[cfg(any(feature = "native-tls", feature = "rustls"))]
let (socket, _) =
tokio_tungstenite::connect_async_tls_with_config(url, config, maybe_connector).await?;
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
let (socket, _) = tokio_tungstenite::connect_async_with_config(url, config).await?;
Ok(socket)
}
impl crate::api::Connection for Client {}
impl Connection for Client {
fn new(method: Method) -> Self {
Self {
id: 0,
method,
}
}
fn connect(
address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
let url = address.endpoint.join(PATH)?;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
let maybe_connector = address.tls_config.map(Connector::from);
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
let maybe_connector = None;
let config = WebSocketConfig {
max_send_queue: match capacity {
0 => None,
capacity => Some(capacity),
},
max_message_size: Some(MAX_MESSAGE_SIZE),
max_frame_size: Some(MAX_FRAME_SIZE),
accept_unmasked_frames: false,
};
let socket = connect(&url, Some(config), maybe_connector.clone()).await?;
let (route_tx, route_rx) = match capacity {
0 => flume::unbounded(),
capacity => flume::bounded(capacity),
};
router(url, maybe_connector, capacity, config, socket, route_rx);
let mut features = HashSet::new();
features.insert(ExtraFeatures::Auth);
Ok(Surreal {
router: OnceCell::with_value(Arc::new(Router {
features,
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
})),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
self.id = router.next_id();
let (sender, receiver) = flume::bounded(1);
let route = Route {
request: (self.id, self.method, param),
response: sender,
};
router.sender.send_async(Some(route)).await?;
Ok(receiver)
})
}
fn recv<R>(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let response = rx.into_recv_async().await?;
match response? {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
Box::pin(async move {
let response = rx.into_recv_async().await?;
match response? {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}
#[allow(clippy::too_many_lines)]
pub(crate) fn router(
url: Url,
maybe_connector: Option<Connector>,
capacity: usize,
config: WebSocketConfig,
mut socket: WebSocketStream<MaybeTlsStream<TcpStream>>,
route_rx: Receiver<Option<Route>>,
) {
tokio::spawn(async move {
let ping = {
let mut request = BTreeMap::new();
request.insert("method".to_owned(), PING_METHOD.into());
let value = Value::from(request);
Message::Binary(value.into())
};
let mut vars = IndexMap::new();
let mut replay = IndexMap::new();
'router: loop {
let (socket_sink, socket_stream) = socket.split();
let mut socket_sink = Socket(Some(socket_sink));
if let Socket(Some(socket_sink)) = &mut socket_sink {
let mut routes = match capacity {
0 => HashMap::new(),
capacity => HashMap::with_capacity(capacity),
};
let mut interval = time::interval(PING_INTERVAL);
// don't bombard the server with pings if we miss some ticks
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
// Delay sending the first ping
interval.tick().await;
let pinger = IntervalStream::new(interval);
let streams = (
socket_stream.map(Either::Response),
route_rx.stream().map(Either::Request),
pinger.map(|_| Either::Ping),
);
let mut merged = streams.merge();
let mut last_activity = Instant::now();
while let Some(either) = merged.next().await {
match either {
Either::Request(Some(Route {
request,
response,
})) => {
let (id, method, param) = request;
let params = match param.query {
Some((query, bindings)) => {
vec![query.to_string().into(), bindings.into()]
}
None => param.other,
};
match method {
Method::Set => {
if let [Value::Strand(Strand(key)), value] = &params[..2] {
vars.insert(key.clone(), value.clone());
}
}
Method::Unset => {
if let [Value::Strand(Strand(key))] = &params[..1] {
vars.remove(key);
}
}
_ => {}
}
let method_str = match method {
Method::Health => PING_METHOD,
_ => method.as_str(),
};
let message = {
let mut request = BTreeMap::new();
request.insert("id".to_owned(), Value::from(id));
request.insert("method".to_owned(), method_str.into());
if !params.is_empty() {
request.insert("params".to_owned(), params.into());
}
let payload = Value::from(request);
trace!(target: LOG, "Request {payload}");
Message::Binary(payload.into())
};
if let Method::Authenticate
| Method::Invalidate
| Method::Signin
| Method::Signup
| Method::Use = method
{
replay.insert(method, message.clone());
}
match socket_sink.send(message).await {
Ok(..) => {
last_activity = Instant::now();
match routes.entry(id) {
Entry::Vacant(entry) => {
entry.insert((method, response));
}
Entry::Occupied(..) => {
let error = Error::DuplicateRequestId(id);
if response
.into_send_async(Err(error.into()))
.await
.is_err()
{
trace!(target: LOG, "Receiver dropped");
}
}
}
}
Err(error) => {
let error = Error::Ws(error.to_string());
if response.into_send_async(Err(error.into())).await.is_err() {
trace!(target: LOG, "Receiver dropped");
}
break;
}
}
}
Either::Response(result) => {
last_activity = Instant::now();
match result {
Ok(message) => match Response::try_from(message) {
Ok(option) => {
if let Some(response) = option {
trace!(target: LOG, "{response:?}");
if let Some(id) = response.id {
if let Some((method, sender)) =
routes.remove(&id.as_int())
{
let _res = sender
.into_send_async(DbResponse::from((
method,
response.content,
)))
.await;
}
}
}
}
Err(_error) => {
trace!(target: LOG, "Failed to deserialise message");
}
},
Err(error) => {
match error {
WsError::ConnectionClosed => {
trace!(
target: LOG,
"Connection successfully closed on the server"
);
}
error => {
trace!(target: LOG, "{error}");
}
}
break;
}
}
}
Either::Ping => {
// only ping if we haven't talked to the server recently
if last_activity.elapsed() >= PING_INTERVAL {
trace!(target: LOG, "Pinging the server");
if let Err(error) = socket_sink.send(ping.clone()).await {
trace!(target: LOG, "failed to ping the server; {error:?}");
break;
}
}
}
Either::Request(None) => {
break 'router;
}
}
}
}
'reconnect: loop {
trace!(target: LOG, "Reconnecting...");
match connect(&url, Some(config), maybe_connector.clone()).await {
Ok(s) => {
socket = s;
for (_, message) in &replay {
if let Err(error) = socket.send(message.clone()).await {
trace!(target: LOG, "{error}");
time::sleep(time::Duration::from_secs(1)).await;
continue 'reconnect;
}
}
#[cfg(feature = "protocol-ws")]
for (key, value) in &vars {
let mut request = BTreeMap::new();
request.insert("method".to_owned(), Method::Set.as_str().into());
request.insert(
"params".to_owned(),
vec![key.as_str().into(), value.clone()].into(),
);
let payload = Value::from(request);
trace!(target: LOG, "Request {payload}");
if let Err(error) = socket.send(Message::Binary(payload.into())).await {
trace!(target: LOG, "{error}");
time::sleep(time::Duration::from_secs(1)).await;
continue 'reconnect;
}
}
trace!(target: LOG, "Reconnected successfully");
break;
}
Err(error) => {
trace!(target: LOG, "Failed to reconnect; {error}");
time::sleep(time::Duration::from_secs(1)).await;
}
}
}
}
});
}
impl Response {
fn try_from(message: Message) -> Result<Option<Self>> {
match message {
Message::Text(text) => {
trace!(target: LOG, "Received an unexpected text message; {text}");
Ok(None)
}
Message::Binary(binary) => msgpack::from_slice(&binary).map(Some).map_err(|error| {
Error::ResponseFromBinary {
binary,
error,
}
.into()
}),
Message::Ping(..) => {
trace!(target: LOG, "Received a ping from the server");
Ok(None)
}
Message::Pong(..) => {
trace!(target: LOG, "Received a pong from the server");
Ok(None)
}
Message::Frame(..) => {
trace!(target: LOG, "Received an unexpected raw frame");
Ok(None)
}
Message::Close(..) => {
trace!(target: LOG, "Received an unexpected close message");
Ok(None)
}
}
}
}
pub struct Socket(Option<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>);
impl Drop for Socket {
fn drop(&mut self) {
if let Some(mut conn) = mem::take(&mut self.0) {
futures::executor::block_on(async move {
match conn.borrow_mut().close().await {
Ok(..) => trace!(target: LOG, "Connection closed successfully"),
Err(error) => {
trace!(target: LOG, "Failed to close database connection; {error}")
}
}
});
}
}
}

View file

@ -0,0 +1,417 @@
use super::LOG;
use super::PATH;
use crate::api::conn::Connection;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
use crate::api::engines::remote::ws::Client;
use crate::api::engines::remote::ws::Response;
use crate::api::engines::remote::ws::PING_INTERVAL;
use crate::api::engines::remote::ws::PING_METHOD;
use crate::api::err::Error;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
use crate::api::ExtraFeatures;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use crate::sql::Strand;
use crate::sql::Value;
use flume::Receiver;
use flume::Sender;
use futures::SinkExt;
use futures::StreamExt;
use futures_concurrency::stream::Merge as _;
use indexmap::IndexMap;
use once_cell::sync::OnceCell;
use pharos::Channel;
use pharos::Observable;
use pharos::ObserveConfig;
use serde::de::DeserializeOwned;
use std::collections::hash_map::Entry;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
use std::time::Instant;
use tokio::time;
use tokio::time::MissedTickBehavior;
use tokio_stream::wrappers::IntervalStream;
use wasm_bindgen_futures::spawn_local;
use ws_stream_wasm::WsEvent;
use ws_stream_wasm::WsMessage as Message;
use ws_stream_wasm::WsMeta;
pub(crate) enum Either {
Request(Option<Route>),
Response(Message),
Event(WsEvent),
Ping,
}
impl crate::api::Connection for Client {}
impl Connection for Client {
fn new(method: Method) -> Self {
Self {
id: 0,
method,
}
}
fn connect(
mut address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
address.endpoint = address.endpoint.join(PATH)?;
let (route_tx, route_rx) = match capacity {
0 => flume::unbounded(),
capacity => flume::bounded(capacity),
};
let (conn_tx, conn_rx) = flume::bounded(1);
router(address, capacity, conn_tx, route_rx);
if let Err(error) = conn_rx.into_recv_async().await? {
return Err(error);
}
let mut features = HashSet::new();
features.insert(ExtraFeatures::Auth);
Ok(Surreal {
router: OnceCell::with_value(Arc::new(Router {
features,
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
})),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
self.id = router.next_id();
let (sender, receiver) = flume::bounded(1);
let route = Route {
request: (self.id, self.method, param),
response: sender,
};
router.sender.send_async(Some(route)).await?;
Ok(receiver)
})
}
fn recv<R>(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let response = rx.into_recv_async().await?;
match response? {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
Box::pin(async move {
let response = rx.into_recv_async().await?;
match response? {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}
pub(crate) fn router(
address: Endpoint,
capacity: usize,
conn_tx: Sender<Result<()>>,
route_rx: Receiver<Option<Route>>,
) {
spawn_local(async move {
let (mut ws, mut socket) = match WsMeta::connect(&address.endpoint, None).await {
Ok(pair) => pair,
Err(error) => {
let _ = conn_tx.into_send_async(Err(error.into())).await;
return;
}
};
let mut events = {
let result = match capacity {
0 => ws.observe(ObserveConfig::default()).await,
capacity => ws.observe(Channel::Bounded(capacity).into()).await,
};
match result {
Ok(events) => events,
Err(error) => {
let _ = conn_tx.into_send_async(Err(error.into())).await;
return;
}
}
};
let _ = conn_tx.into_send_async(Ok(())).await;
let ping = {
let mut request = BTreeMap::new();
request.insert("method".to_owned(), PING_METHOD.into());
let value = Value::from(request);
Message::Binary(value.into())
};
let mut vars = IndexMap::new();
let mut replay = IndexMap::new();
'router: loop {
let (mut socket_sink, socket_stream) = socket.split();
let mut routes = match capacity {
0 => HashMap::new(),
capacity => HashMap::with_capacity(capacity),
};
let mut interval = time::interval(PING_INTERVAL);
// don't bombard the server with pings if we miss some ticks
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
// Delay sending the first ping
interval.tick().await;
let pinger = IntervalStream::new(interval);
let streams = (
socket_stream.map(Either::Response),
route_rx.stream().map(Either::Request),
pinger.map(|_| Either::Ping),
events.map(Either::Event),
);
let mut merged = streams.merge();
let mut last_activity = Instant::now();
while let Some(either) = merged.next().await {
match either {
Either::Request(Some(Route {
request,
response,
})) => {
let (id, method, param) = request;
let params = match param.query {
Some((query, bindings)) => {
vec![query.to_string().into(), bindings.into()]
}
None => param.other,
};
match method {
Method::Set => {
if let [Value::Strand(Strand(key)), value] = &params[..2] {
vars.insert(key.to_owned(), value.clone());
}
}
Method::Unset => {
if let [Value::Strand(Strand(key))] = &params[..1] {
vars.remove(key);
}
}
_ => {}
}
let method_str = match method {
Method::Health => PING_METHOD,
_ => method.as_str(),
};
let message = {
let mut request = BTreeMap::new();
request.insert("id".to_owned(), Value::from(id));
request.insert("method".to_owned(), method_str.into());
if !params.is_empty() {
request.insert("params".to_owned(), params.into());
}
let payload = Value::from(request);
trace!(target: LOG, "Request {payload}");
Message::Binary(payload.into())
};
if let Method::Authenticate
| Method::Invalidate
| Method::Signin
| Method::Signup
| Method::Use = method
{
replay.insert(method, message.clone());
}
match socket_sink.send(message).await {
Ok(..) => {
last_activity = Instant::now();
match routes.entry(id) {
Entry::Vacant(entry) => {
entry.insert((method, response));
}
Entry::Occupied(..) => {
let error = Error::DuplicateRequestId(id);
if response
.into_send_async(Err(error.into()))
.await
.is_err()
{
trace!(target: LOG, "Receiver dropped");
}
}
}
}
Err(error) => {
let error = Error::Ws(error.to_string());
if response.into_send_async(Err(error.into())).await.is_err() {
trace!(target: LOG, "Receiver dropped");
}
break;
}
}
}
Either::Response(message) => {
last_activity = Instant::now();
match Response::try_from(message) {
Ok(option) => {
if let Some(response) = option {
trace!(target: LOG, "{response:?}");
if let Some(id) = response.id {
if let Some((method, sender)) = routes.remove(&id.as_int())
{
let _ = sender
.into_send_async(DbResponse::from((
method,
response.content,
)))
.await;
}
}
}
}
Err(_error) => {
trace!(target: LOG, "Failed to deserialise message");
}
}
}
Either::Event(event) => match event {
WsEvent::Error => {
trace!(target: LOG, "connection errored");
break;
}
WsEvent::WsErr(error) => {
trace!(target: LOG, "{error}");
}
WsEvent::Closed(..) => {
trace!(target: LOG, "connection closed");
break;
}
_ => {}
},
Either::Ping => {
// only ping if we haven't talked to the server recently
if last_activity.elapsed() >= PING_INTERVAL {
trace!(target: LOG, "Pinging the server");
if let Err(error) = socket_sink.send(ping.clone()).await {
trace!(target: LOG, "failed to ping the server; {error:?}");
break;
}
}
}
Either::Request(None) => {
break 'router;
}
}
}
'reconnect: loop {
trace!(target: LOG, "Reconnecting...");
match WsMeta::connect(&address.endpoint, None).await {
Ok((mut meta, stream)) => {
socket = stream;
events = {
let result = match capacity {
0 => meta.observe(ObserveConfig::default()).await,
capacity => meta.observe(Channel::Bounded(capacity).into()).await,
};
match result {
Ok(events) => events,
Err(error) => {
trace!(target: LOG, "{error}");
time::sleep(time::Duration::from_secs(1)).await;
continue 'reconnect;
}
}
};
for (_, message) in &replay {
if let Err(error) = socket.send(message.clone()).await {
trace!(target: LOG, "{error}");
time::sleep(time::Duration::from_secs(1)).await;
continue 'reconnect;
}
}
for (key, value) in &vars {
let mut request = BTreeMap::new();
request.insert("method".to_owned(), Method::Set.as_str().into());
request.insert(
"params".to_owned(),
vec![key.as_str().into(), value.clone()].into(),
);
let payload = Value::from(request);
trace!(target: LOG, "Request {payload}");
if let Err(error) = socket.send(Message::Binary(payload.into())).await {
trace!(target: LOG, "{error}");
time::sleep(time::Duration::from_secs(1)).await;
continue 'reconnect;
}
}
trace!(target: LOG, "Reconnected successfully");
break;
}
Err(error) => {
trace!(target: LOG, "Failed to reconnect; {error}");
time::sleep(time::Duration::from_secs(1)).await;
}
}
}
}
});
}
impl Response {
fn try_from(message: Message) -> Result<Option<Self>> {
match message {
Message::Text(text) => {
trace!(target: LOG, "Received an unexpected text message; {text}");
Ok(None)
}
Message::Binary(binary) => msgpack::from_slice(&binary).map(Some).map_err(|error| {
Error::ResponseFromBinary {
binary,
error,
}
.into()
}),
}
}
}

202
lib/src/api/err/mod.rs Normal file
View file

@ -0,0 +1,202 @@
use crate::api::Response;
use crate::sql::Array;
use crate::sql::Edges;
use crate::sql::Object;
use crate::sql::Thing;
use crate::sql::Value;
use std::io;
use std::path::PathBuf;
use thiserror::Error;
/// An error originating from a remote SurrealDB database.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
/// There was an error processing the query
#[error("{0}")]
Query(String),
/// There was an error processing a remote HTTP request
#[error("There was an error processing a remote HTTP request")]
Http(String),
/// There was an error processing a remote WS request
#[error("There was an error processing a remote WS request")]
Ws(String),
/// There specified scheme does not match any supported protocol or storage engine
#[error("Unsupported protocol or storage engine, `{0}`")]
Scheme(String),
/// Tried to run database queries without initialising the connection first
#[error("Connection uninitialised")]
ConnectionUninitialised,
/// `Query::bind` not called with an object nor a key/value tuple
#[error("Invalid bindings: {0}")]
InvalidBindings(Value),
/// Tried to use a range query on a record ID
#[error("Range on record IDs not supported: {0}")]
RangeOnRecordId(Thing),
/// Tried to use a range query on an object
#[error("Range on objects not supported: {0}")]
RangeOnObject(Object),
/// Tried to use a range query on an array
#[error("Range on arrays not supported: {0}")]
RangeOnArray(Array),
/// Tried to use a range query on an edge or edges
#[error("Range on edges not supported: {0}")]
RangeOnEdges(Edges),
/// Tried to use `table:id` syntax as a method parameter when `(table, id)` should be used instead
#[error("`{table}:{id}` is not allowed as a method parameter; try `({table}, {id})`")]
TableColonId {
table: String,
id: String,
},
/// Duplicate request ID
#[error("Duplicate request ID: {0}")]
DuplicateRequestId(i64),
/// Invalid request
#[error("Invalid request: {0}")]
InvalidRequest(String),
/// Invalid params
#[error("Invalid params: {0}")]
InvalidParams(String),
/// Internal server error
#[error("Internal error: {0}")]
InternalError(String),
/// Parse error
#[error("Parse error: {0}")]
ParseError(String),
/// Invalid semantic version
#[error("Invalid semantic version: {0}")]
InvalidSemanticVersion(String),
/// Invalid URL
#[error("Invalid URL: {0}")]
InvalidUrl(String),
/// Failed to convert a `sql::Value` to `T`
#[error("Failed to convert `{value}` to `T`: {error}")]
FromValue {
value: Value,
error: String,
},
/// Failed to deserialize a binary response
#[error("Failed to deserialize a binary response: {error}")]
ResponseFromBinary {
binary: Vec<u8>,
error: msgpack::decode::Error,
},
/// Failed to serialize `sql::Value` to JSON string
#[error("Failed to serialize `{value}` to JSON string: {error}")]
ToJsonString {
value: Value,
error: String,
},
/// Failed to serialize `sql::Value` to JSON string
#[error("Failed to serialize `{string}` to JSON string: {error}")]
FromJsonString {
string: String,
error: String,
},
/// Invalid namespace name
#[error("Invalid namespace name: {0:?}")]
InvalidNsName(String),
/// Invalid database name
#[error("Invalid database name: {0:?}")]
InvalidDbName(String),
/// File open error
#[error("Failed to open `{path}`: {error}")]
FileOpen {
path: PathBuf,
error: io::Error,
},
/// File read error
#[error("Failed to read `{path}`: {error}")]
FileRead {
path: PathBuf,
error: io::Error,
},
/// Tried to take only a single result when the query returned multiple records
#[error("Tried to take only a single result from a query that contains multiple")]
LossyTake(Response),
/// The protocol or storage engine being used does not support backups on the architecture
/// it's running on
#[error("The protocol or storage engine does not support backups on this architecture")]
BackupsNotSupported,
/// The protocol or storage engine being used does not support authentication on the
/// architecture it's running on
#[error("The protocol or storage engine does not support authentication on this architecture")]
AuthNotSupported,
}
#[cfg(feature = "protocol-http")]
impl From<reqwest::Error> for crate::Error {
fn from(e: reqwest::Error) -> Self {
Self::Api(Error::Http(e.to_string()))
}
}
#[cfg(all(feature = "protocol-ws", not(target_arch = "wasm32")))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "protocol-ws", not(target_arch = "wasm32")))))]
impl From<tokio_tungstenite::tungstenite::Error> for crate::Error {
fn from(error: tokio_tungstenite::tungstenite::Error) -> Self {
Self::Api(Error::Ws(error.to_string()))
}
}
impl<T> From<flume::SendError<T>> for crate::Error {
fn from(error: flume::SendError<T>) -> Self {
Self::Api(Error::InternalError(error.to_string()))
}
}
impl From<flume::RecvError> for crate::Error {
fn from(error: flume::RecvError) -> Self {
Self::Api(Error::InternalError(error.to_string()))
}
}
impl From<url::ParseError> for crate::Error {
fn from(error: url::ParseError) -> Self {
Self::Api(Error::InternalError(error.to_string()))
}
}
#[cfg(all(feature = "protocol-ws", target_arch = "wasm32"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "protocol-ws", target_arch = "wasm32"))))]
impl From<ws_stream_wasm::WsErr> for crate::Error {
fn from(error: ws_stream_wasm::WsErr) -> Self {
Self::Api(Error::Ws(error.to_string()))
}
}
#[cfg(all(feature = "protocol-ws", target_arch = "wasm32"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "protocol-ws", target_arch = "wasm32"))))]
impl From<pharos::PharErr> for crate::Error {
fn from(error: pharos::PharErr) -> Self {
Self::Api(Error::Ws(error.to_string()))
}
}

View file

@ -0,0 +1,37 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::opt::auth::Jwt;
use crate::api::Connection;
use crate::api::Error;
use crate::api::ExtraFeatures;
use crate::api::Result;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// An authentication future
#[derive(Debug)]
pub struct Authenticate<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) token: Jwt,
}
impl<'r, Client> IntoFuture for Authenticate<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let router = self.router?;
if !router.features.contains(&ExtraFeatures::Auth) {
return Err(Error::AuthNotSupported.into());
}
let mut conn = Client::new(Method::Authenticate);
conn.execute(router, Param::new(vec![self.token.into()])).await
})
}
}

View file

@ -0,0 +1,69 @@
use crate::api::method::Cancel;
use crate::api::method::Commit;
use crate::api::Connection;
use crate::api::Result;
use crate::api::Surreal;
use crate::sql::statements::BeginStatement;
use std::future::Future;
use std::future::IntoFuture;
use std::ops::Deref;
use std::pin::Pin;
/// A beginning of a transaction
#[derive(Debug)]
pub struct Begin<C: Connection> {
pub(super) client: Surreal<C>,
}
impl<C> IntoFuture for Begin<C>
where
C: Connection,
{
type Output = Result<Transaction<C>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'static>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
self.client.query(BeginStatement).await?;
Ok(Transaction {
client: self.client,
})
})
}
}
/// An ongoing transaction
#[derive(Debug)]
pub struct Transaction<C: Connection> {
client: Surreal<C>,
}
impl<C> Transaction<C>
where
C: Connection,
{
/// Creates a commit future
pub fn commit(self) -> Commit<C> {
Commit {
client: self.client,
}
}
/// Creates a cancel future
pub fn cancel(self) -> Cancel<C> {
Cancel {
client: self.client,
}
}
}
impl<C> Deref for Transaction<C>
where
C: Connection,
{
type Target = Surreal<C>;
fn deref(&self) -> &Self::Target {
&self.client
}
}

View file

@ -0,0 +1,28 @@
use crate::api::Connection;
use crate::api::Result;
use crate::api::Surreal;
use crate::sql::statements::CancelStatement;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A transaction cancellation future
#[derive(Debug)]
pub struct Cancel<C: Connection> {
pub(crate) client: Surreal<C>,
}
impl<C> IntoFuture for Cancel<C>
where
C: Connection,
{
type Output = Result<Surreal<C>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'static>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
self.client.query(CancelStatement).await?;
Ok(self.client)
})
}
}

View file

@ -0,0 +1,28 @@
use crate::api::Connection;
use crate::api::Result;
use crate::api::Surreal;
use crate::sql::statements::CommitStatement;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A transaction commit future
#[derive(Debug)]
pub struct Commit<C: Connection> {
pub(crate) client: Surreal<C>,
}
impl<C> IntoFuture for Commit<C>
where
C: Connection,
{
type Output = Result<Surreal<C>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'static>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
self.client.query(CommitStatement).await?;
Ok(self.client)
})
}
}

View file

@ -0,0 +1,65 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::opt::from_json;
use crate::api::opt::Range;
use crate::api::opt::Resource;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Id;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::json;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// A content future
///
/// Content inserts or replaces the contents of a record entirely
#[derive(Debug)]
pub struct Content<'r, C: Connection, D, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) method: Method,
pub(super) resource: Result<Resource>,
pub(super) range: Option<Range<Id>>,
pub(super) content: D,
pub(super) response_type: PhantomData<R>,
}
impl<'r, C, D, R> Content<'r, C, D, R>
where
C: Connection,
D: Serialize,
{
fn split(self) -> Result<(&'r Router<C>, Method, Param)> {
let resource = self.resource?;
let param = match self.range {
Some(range) => resource.with_range(range)?,
None => resource.into(),
};
let content = json!(self.content);
let param = Param::new(vec![param, from_json(content)]);
Ok((self.router?, self.method, param))
}
}
impl<'r, Client, D, R> IntoFuture for Content<'r, Client, D, R>
where
Client: Connection,
D: Serialize,
R: DeserializeOwned + Send + Sync,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
let result = self.split();
Box::pin(async move {
let (router, method, param) = result?;
let mut conn = Client::new(method);
conn.execute(router, param).await
})
}
}

View file

@ -0,0 +1,87 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::method::Content;
use crate::api::opt::Resource;
use crate::api::Connection;
use crate::api::Result;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// A record create future
#[derive(Debug)]
pub struct Create<'r, C: Connection, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) resource: Result<Resource>,
pub(super) response_type: PhantomData<R>,
}
impl<'r, Client, R> Create<'r, Client, R>
where
Client: Connection,
{
async fn execute<T>(self) -> Result<T>
where
T: DeserializeOwned,
{
let mut conn = Client::new(Method::Create);
conn.execute(self.router?, Param::new(vec![self.resource?.into()])).await
}
}
impl<'r, Client, R> IntoFuture for Create<'r, Client, Option<R>>
where
Client: Connection,
R: DeserializeOwned + Send + Sync + 'r,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
impl<'r, Client, R> IntoFuture for Create<'r, Client, Vec<R>>
where
Client: Connection,
R: DeserializeOwned + Send + Sync + 'r,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
macro_rules! create_methods {
($this:ty) => {
impl<'r, C, R> Create<'r, C, $this>
where
C: Connection,
{
/// Sets content of a record
pub fn content<D>(self, data: D) -> Content<'r, C, D, R>
where
D: Serialize,
{
Content {
router: self.router,
method: Method::Create,
resource: self.resource,
range: None,
content: data,
response_type: PhantomData,
}
}
}
};
}
create_methods!(Option<R>);
create_methods!(Vec<R>);

View file

@ -0,0 +1,71 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::opt::Range;
use crate::api::opt::Resource;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Id;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// A record delete future
#[derive(Debug)]
pub struct Delete<'r, C: Connection, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) resource: Result<Resource>,
pub(super) range: Option<Range<Id>>,
pub(super) response_type: PhantomData<R>,
}
impl<'r, Client, R> Delete<'r, Client, R>
where
Client: Connection,
{
async fn execute(self) -> Result<()> {
let resource = self.resource?;
let param = match self.range {
Some(range) => resource.with_range(range)?,
None => resource.into(),
};
let mut conn = Client::new(Method::Delete);
conn.execute(self.router?, Param::new(vec![param])).await
}
}
impl<'r, Client> IntoFuture for Delete<'r, Client, Option<()>>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
impl<'r, Client> IntoFuture for Delete<'r, Client, Vec<()>>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
impl<C> Delete<'_, C, Vec<()>>
where
C: Connection,
{
/// Restricts a range of records to delete
pub fn range(mut self, bounds: impl Into<Range<Id>>) -> Self {
self.range = Some(bounds.into());
self
}
}

View file

@ -0,0 +1,37 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Error;
use crate::api::ExtraFeatures;
use crate::api::Result;
use std::future::Future;
use std::future::IntoFuture;
use std::path::PathBuf;
use std::pin::Pin;
/// A database export future
#[derive(Debug)]
pub struct Export<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) file: PathBuf,
}
impl<'r, Client> IntoFuture for Export<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async {
let router = self.router?;
if !router.features.contains(&ExtraFeatures::Backup) {
return Err(Error::BackupsNotSupported.into());
}
let mut conn = Client::new(Method::Export);
conn.execute(router, Param::file(self.file)).await
})
}
}

View file

@ -0,0 +1,29 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Result;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A health check future
#[derive(Debug)]
pub struct Health<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
}
impl<'r, Client> IntoFuture for Health<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async {
let mut conn = Client::new(Method::Health);
conn.execute(self.router?, Param::new(Vec::new())).await
})
}
}

View file

@ -0,0 +1,37 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Error;
use crate::api::ExtraFeatures;
use crate::api::Result;
use std::future::Future;
use std::future::IntoFuture;
use std::path::PathBuf;
use std::pin::Pin;
/// An database import future
#[derive(Debug)]
pub struct Import<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) file: PathBuf,
}
impl<'r, Client> IntoFuture for Import<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async {
let router = self.router?;
if !router.features.contains(&ExtraFeatures::Backup) {
return Err(Error::BackupsNotSupported.into());
}
let mut conn = Client::new(Method::Import);
conn.execute(router, Param::file(self.file)).await
})
}
}

View file

@ -0,0 +1,35 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Error;
use crate::api::ExtraFeatures;
use crate::api::Result;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A session invalidate future
#[derive(Debug)]
pub struct Invalidate<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
}
impl<'r, Client> IntoFuture for Invalidate<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async {
let router = self.router?;
if !router.features.contains(&ExtraFeatures::Auth) {
return Err(Error::AuthNotSupported.into());
}
let mut conn = Client::new(Method::Invalidate);
conn.execute(router, Param::new(Vec::new())).await
})
}
}

View file

@ -0,0 +1,31 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Uuid;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A live query kill future
#[derive(Debug)]
pub struct Kill<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) query_id: Uuid,
}
impl<'r, Client> IntoFuture for Kill<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut conn = Client::new(Method::Kill);
conn.execute(self.router?, Param::new(vec![self.query_id.into()])).await
})
}
}

View file

@ -0,0 +1,33 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Table;
use crate::sql::Uuid;
use crate::sql::Value;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A live query future
#[derive(Debug)]
pub struct Live<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) table_name: String,
}
impl<'r, Client> IntoFuture for Live<'r, Client>
where
Client: Connection,
{
type Output = Result<Uuid>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut conn = Client::new(Method::Live);
conn.execute(self.router?, Param::new(vec![Value::Table(Table(self.table_name))])).await
})
}
}

View file

@ -0,0 +1,62 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::opt::from_json;
use crate::api::opt::Range;
use crate::api::opt::Resource;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Id;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::json;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// A merge future
#[derive(Debug)]
pub struct Merge<'r, C: Connection, D, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) resource: Result<Resource>,
pub(super) range: Option<Range<Id>>,
pub(super) content: D,
pub(super) response_type: PhantomData<R>,
}
impl<'r, C, D, R> Merge<'r, C, D, R>
where
C: Connection,
D: Serialize,
{
fn split(self) -> Result<(&'r Router<C>, Method, Param)> {
let resource = self.resource?;
let param = match self.range {
Some(range) => resource.with_range(range)?,
None => resource.into(),
};
let content = json!(self.content);
let param = Param::new(vec![param, from_json(content)]);
Ok((self.router?, Method::Merge, param))
}
}
impl<'r, Client, D, R> IntoFuture for Merge<'r, Client, D, R>
where
Client: Connection,
D: Serialize,
R: DeserializeOwned + Send + Sync,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
let result = self.split();
Box::pin(async move {
let (router, method, param) = result?;
let mut conn = Client::new(method);
conn.execute(router, param).await
})
}
}

1007
lib/src/api/method/mod.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::opt::PatchOp;
use crate::api::opt::Range;
use crate::api::opt::Resource;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Array;
use crate::sql::Id;
use crate::sql::Value;
use serde::de::DeserializeOwned;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// A patch future
#[derive(Debug)]
pub struct Patch<'r, C: Connection, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) resource: Result<Resource>,
pub(super) range: Option<Range<Id>>,
pub(super) patches: Vec<Value>,
pub(super) response_type: PhantomData<R>,
}
impl<'r, C, R> Patch<'r, C, R>
where
C: Connection,
{
/// Applies JSON Patch changes to all records, or a specific record, in the database.
pub fn patch(mut self, PatchOp(patch): PatchOp) -> Patch<'r, C, R> {
self.patches.push(patch);
self
}
}
impl<'r, Client, R> IntoFuture for Patch<'r, Client, R>
where
Client: Connection,
R: DeserializeOwned + Send + Sync,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let resource = self.resource?;
let param = match self.range {
Some(range) => resource.with_range(range)?,
None => resource.into(),
};
let patches = Value::Array(Array(self.patches));
let mut conn = Client::new(Method::Patch);
conn.execute(self.router?, Param::new(vec![param, patches])).await
})
}
}

500
lib/src/api/method/query.rs Normal file
View file

@ -0,0 +1,500 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::err::Error;
use crate::api::opt;
use crate::api::opt::from_json;
use crate::api::Connection;
use crate::api::Result;
use crate::sql;
use crate::sql::Array;
use crate::sql::Object;
use crate::sql::Statement;
use crate::sql::Statements;
use crate::sql::Strand;
use crate::sql::Value;
use indexmap::IndexMap;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::json;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::future::Future;
use std::future::IntoFuture;
use std::mem;
use std::pin::Pin;
/// A query future
#[derive(Debug)]
pub struct Query<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) query: Vec<Result<Vec<Statement>>>,
pub(super) bindings: Result<BTreeMap<String, Value>>,
}
impl<'r, Client> IntoFuture for Query<'r, Client>
where
Client: Connection,
{
type Output = Result<Response>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut statements = Vec::with_capacity(self.query.len());
for query in self.query {
statements.extend(query?);
}
let query = sql::Query(Statements(statements));
let param = Param::query(query, self.bindings?);
let mut conn = Client::new(Method::Query);
conn.execute_query(self.router?, param).await
})
}
}
impl<'r, C> Query<'r, C>
where
C: Connection,
{
/// Chains a query onto an existing query
pub fn query(mut self, query: impl opt::IntoQuery) -> Self {
self.query.push(query.into_query());
self
}
/// Binds a parameter or parameters to a query
///
/// # Examples
///
/// Binding a key/value tuple
///
/// ```no_run
/// use surrealdb::sql;
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// # let db = surrealdb::engines::any::connect("mem://").await?;
/// let response = db.query(sql!(CREATE user SET name = $name))
/// .bind(("name", "John Doe"))
/// .await?;
/// # Ok(())
/// # }
/// ```
///
/// Binding an object
///
/// ```no_run
/// use serde::Serialize;
/// use surrealdb::sql;
///
/// #[derive(Serialize)]
/// struct User<'a> {
/// name: &'a str,
/// }
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// # let db = surrealdb::engines::any::connect("mem://").await?;
/// let response = db.query(sql!(CREATE user SET name = $name))
/// .bind(User {
/// name: "John Doe",
/// })
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn bind(mut self, bindings: impl Serialize) -> Self {
if let Ok(current) = &mut self.bindings {
let mut bindings = from_json(json!(bindings));
if let Value::Array(Array(array)) = &mut bindings {
if let [Value::Strand(Strand(key)), value] = &mut array[..] {
let mut map = BTreeMap::new();
map.insert(mem::take(key), mem::take(value));
bindings = map.into();
}
}
match &mut bindings {
Value::Object(Object(map)) => current.append(map),
_ => {
self.bindings = Err(Error::InvalidBindings(bindings).into());
}
}
}
self
}
}
pub(crate) type QueryResult = Result<Vec<Value>>;
/// The response type of a `Surreal::query` request
#[derive(Debug)]
pub struct Response(pub(crate) IndexMap<usize, QueryResult>);
impl Response {
/// Takes and returns records returned from the database
///
/// A query that only returns one result can be deserialized into an
/// `Option<T>`, while those that return multiple results should be
/// deserialized into a `Vec<T>`.
///
/// # Examples
///
/// ```no_run
/// use serde::Deserialize;
/// use surrealdb::sql;
///
/// #[derive(Debug, Deserialize)]
/// # #[allow(dead_code)]
/// struct User {
/// id: String,
/// balance: String
/// }
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// # let db = surrealdb::engines::any::connect("mem://").await?;
/// #
/// let mut response = db
/// // Get `john`'s details
/// .query(sql!(SELECT * FROM user:john))
/// // List all users whose first name is John
/// .query(sql!(SELECT * FROM user WHERE name.first = "John"))
/// // Get John's address
/// .query(sql!(SELECT address FROM user:john))
/// // Get all users' addresses
/// .query(sql!(SELECT address FROM user))
/// .await?;
///
/// // Get the first (and only) user from the first query
/// let user: Option<User> = response.take(0)?;
///
/// // Get all users from the second query
/// let users: Vec<User> = response.take(1)?;
///
/// // Retrieve John's address without making a special struct for it
/// let address: Option<String> = response.take((2, "address"))?;
///
/// // Get all users' addresses
/// let addresses: Vec<String> = response.take((3, "address"))?;
///
/// // You can continue taking more fields on the same response
/// // object when extracting individual fields
/// let mut response = db.query(sql!(SELECT * FROM user)).await?;
///
/// // Since the query we want to access is at index 0, we can use
/// // a shortcut instead of `response.take((0, "field"))`
/// let ids: Vec<String> = response.take("id")?;
/// let names: Vec<String> = response.take("name")?;
/// let addresses: Vec<String> = response.take("address")?;
/// #
/// # Ok(())
/// # }
/// ```
///
/// The indices are stable. Taking one index doesn't affect the numbering
/// of the other indices, so you can take them in any order you see fit.
pub fn take<R>(&mut self, index: impl opt::QueryResult<R>) -> Result<R>
where
R: DeserializeOwned,
{
index.query_result(self)
}
/// Take all errors from the query response
///
/// The errors are keyed by the corresponding index of the statement that failed.
/// Afterwards the response is left with only statements that did not produce any errors.
///
/// # Examples
///
/// ```no_run
/// use surrealdb::sql;
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// # let db = surrealdb::engines::any::connect("mem://").await?;
/// # let mut response = db.query(sql!(SELECT * FROM user)).await?;
/// let errors = response.take_errors();
/// # Ok(())
/// # }
/// ```
pub fn take_errors(&mut self) -> HashMap<usize, crate::Error> {
let mut keys = Vec::new();
for (key, result) in &self.0 {
if result.is_err() {
keys.push(*key);
}
}
let mut errors = HashMap::with_capacity(keys.len());
for key in keys {
if let Some(Err(error)) = self.0.remove(&key) {
errors.insert(key, error);
}
}
errors
}
/// Check query response for errors and return the first error, if any, or the response
///
/// # Examples
///
/// ```no_run
/// use surrealdb::sql;
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// # let db = surrealdb::engines::any::connect("mem://").await?;
/// # let response = db.query(sql!(SELECT * FROM user)).await?;
/// response.check()?;
/// # Ok(())
/// # }
/// ```
pub fn check(mut self) -> Result<Self> {
let mut first_error = None;
for (key, result) in &self.0 {
if result.is_err() {
first_error = Some(*key);
break;
}
}
if let Some(key) = first_error {
if let Some(Err(error)) = self.0.remove(&key) {
return Err(error);
}
}
Ok(self)
}
/// Returns the number of statements in the query
///
/// # Examples
///
/// ```no_run
/// use surrealdb::sql;
///
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// # let db = surrealdb::engines::any::connect("mem://").await?;
/// let response = db.query(sql!(SELECT * FROM user:john; SELECT * FROM user;)).await?;
///
/// assert_eq!(response.num_statements(), 2);
/// #
/// # Ok(())
/// # }
pub fn num_statements(&self) -> usize {
self.0.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Error::Api;
use serde::Deserialize;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Summary {
title: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Article {
title: String,
body: String,
}
fn to_map(vec: Vec<QueryResult>) -> IndexMap<usize, QueryResult> {
vec.into_iter().enumerate().collect()
}
#[test]
fn take_from_an_empty_response() {
let mut response = Response(Default::default());
let option: Option<String> = response.take(0).unwrap();
assert!(option.is_none());
let mut response = Response(Default::default());
let vec: Vec<String> = response.take(0).unwrap();
assert!(vec.is_empty());
}
#[test]
fn take_from_an_errored_query() {
let mut response = Response(to_map(vec![Err(Error::ConnectionUninitialised.into())]));
response.take::<Option<()>>(0).unwrap_err();
}
#[test]
fn take_from_empty_records() {
let mut response = Response(to_map(vec![Ok(vec![])]));
let option: Option<String> = response.take(0).unwrap();
assert!(option.is_none());
let mut response = Response(to_map(vec![Ok(vec![])]));
let vec: Vec<String> = response.take(0).unwrap();
assert!(vec.is_empty());
}
#[test]
fn take_from_a_scalar_response() {
let scalar = 265;
let mut response = Response(to_map(vec![Ok(vec![scalar.into()])]));
let option: Option<_> = response.take(0).unwrap();
assert_eq!(option, Some(scalar));
let mut response = Response(to_map(vec![Ok(vec![scalar.into()])]));
let vec: Vec<usize> = response.take(0).unwrap();
assert_eq!(vec, vec![scalar]);
let scalar = true;
let mut response = Response(to_map(vec![Ok(vec![scalar.into()])]));
let option: Option<_> = response.take(0).unwrap();
assert_eq!(option, Some(scalar));
let mut response = Response(to_map(vec![Ok(vec![scalar.into()])]));
let vec: Vec<bool> = response.take(0).unwrap();
assert_eq!(vec, vec![scalar]);
}
#[test]
fn take_preserves_order() {
let mut response = Response(to_map(vec![
Ok(vec![0.into()]),
Ok(vec![1.into()]),
Ok(vec![2.into()]),
Ok(vec![3.into()]),
Ok(vec![4.into()]),
Ok(vec![5.into()]),
Ok(vec![6.into()]),
Ok(vec![7.into()]),
]));
let Some(four): Option<i32> = response.take(4).unwrap() else {
panic!("query not found");
};
assert_eq!(four, 4);
let Some(six): Option<i32> = response.take(6).unwrap() else {
panic!("query not found");
};
assert_eq!(six, 6);
let Some(zero): Option<i32> = response.take(0).unwrap() else {
panic!("query not found");
};
assert_eq!(zero, 0);
let Some(one): Option<i32> = response.take(1).unwrap() else {
panic!("query not found");
};
assert_eq!(one, 1);
}
#[test]
fn take_key() {
let summary = Summary {
title: "Lorem Ipsum".to_owned(),
};
let mut response = Response(to_map(vec![Ok(vec![from_json(json!(summary.clone()))])]));
let Some(title): Option<String> = response.take("title").unwrap() else {
panic!("title not found");
};
assert_eq!(title, summary.title);
let mut response = Response(to_map(vec![Ok(vec![from_json(json!(summary.clone()))])]));
let vec: Vec<String> = response.take("title").unwrap();
assert_eq!(vec, vec![summary.title]);
let article = Article {
title: "Lorem Ipsum".to_owned(),
body: "Lorem Ipsum Lorem Ipsum".to_owned(),
};
let mut response = Response(to_map(vec![Ok(vec![from_json(json!(article.clone()))])]));
let Some(title): Option<String> = response.take("title").unwrap() else {
panic!("title not found");
};
assert_eq!(title, article.title);
let Some(body): Option<String> = response.take("body").unwrap() else {
panic!("body not found");
};
assert_eq!(body, article.body);
let mut response = Response(to_map(vec![Ok(vec![from_json(json!(article.clone()))])]));
let vec: Vec<String> = response.take("title").unwrap();
assert_eq!(vec, vec![article.title]);
}
#[test]
fn take_partial_records() {
let mut response = Response(to_map(vec![Ok(vec![true.into(), false.into()])]));
let vec: Vec<bool> = response.take(0).unwrap();
assert_eq!(vec, vec![true, false]);
let mut response = Response(to_map(vec![Ok(vec![true.into(), false.into()])]));
let Err(Api(Error::LossyTake(Response(mut map)))): Result<Option<bool>> = response.take(0) else {
panic!("silently dropping records not allowed");
};
let records = map.remove(&0).unwrap().unwrap();
assert_eq!(records, vec![true.into(), false.into()]);
}
#[test]
fn check_returns_the_first_error() {
let response = vec![
Ok(vec![0.into()]),
Ok(vec![1.into()]),
Ok(vec![2.into()]),
Err(Error::ConnectionUninitialised.into()),
Ok(vec![3.into()]),
Ok(vec![4.into()]),
Ok(vec![5.into()]),
Err(Error::BackupsNotSupported.into()),
Ok(vec![6.into()]),
Ok(vec![7.into()]),
Err(Error::AuthNotSupported.into()),
];
let response = Response(to_map(response));
let crate::Error::Api(Error::ConnectionUninitialised) = response.check().unwrap_err() else {
panic!("check did not return the first error");
};
}
#[test]
fn take_errors() {
let response = vec![
Ok(vec![0.into()]),
Ok(vec![1.into()]),
Ok(vec![2.into()]),
Err(Error::ConnectionUninitialised.into()),
Ok(vec![3.into()]),
Ok(vec![4.into()]),
Ok(vec![5.into()]),
Err(Error::BackupsNotSupported.into()),
Ok(vec![6.into()]),
Ok(vec![7.into()]),
Err(Error::AuthNotSupported.into()),
];
let mut response = Response(to_map(response));
let errors = response.take_errors();
assert_eq!(response.num_statements(), 8);
assert_eq!(errors.len(), 3);
let crate::Error::Api(Error::AuthNotSupported) = errors.get(&10).unwrap() else {
panic!("index `10` is not `AuthNotSupported`");
};
let crate::Error::Api(Error::BackupsNotSupported) = errors.get(&7).unwrap() else {
panic!("index `7` is not `BackupsNotSupported`");
};
let crate::Error::Api(Error::ConnectionUninitialised) = errors.get(&3).unwrap() else {
panic!("index `3` is not `ConnectionUninitialised`");
};
let Some(value): Option<i32> = response.take(2).unwrap() else {
panic!("statement not found");
};
assert_eq!(value, 2);
let Some(value): Option<i32> = response.take(4).unwrap() else {
panic!("statement not found");
};
assert_eq!(value, 3);
}
}

View file

@ -0,0 +1,77 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::opt::Range;
use crate::api::opt::Resource;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Id;
use serde::de::DeserializeOwned;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// A select future
#[derive(Debug)]
pub struct Select<'r, C: Connection, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) resource: Result<Resource>,
pub(super) range: Option<Range<Id>>,
pub(super) response_type: PhantomData<R>,
}
impl<'r, Client, R> Select<'r, Client, R>
where
Client: Connection,
{
async fn execute<T>(self) -> Result<T>
where
T: DeserializeOwned,
{
let resource = self.resource?;
let param = match self.range {
Some(range) => resource.with_range(range)?,
None => resource.into(),
};
let mut conn = Client::new(Method::Select);
conn.execute(self.router?, Param::new(vec![param])).await
}
}
impl<'r, Client, R> IntoFuture for Select<'r, Client, Option<R>>
where
Client: Connection,
R: DeserializeOwned + Send + Sync + 'r,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
impl<'r, Client, R> IntoFuture for Select<'r, Client, Vec<R>>
where
Client: Connection,
R: DeserializeOwned + Send + Sync + 'r,
{
type Output = Result<Vec<R>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
impl<C, R> Select<'_, C, Vec<R>>
where
C: Connection,
{
/// Restricts the records selected to those in the specified range
pub fn range(mut self, bounds: impl Into<Range<Id>>) -> Self {
self.range = Some(bounds.into());
self
}
}

32
lib/src/api/method/set.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Value;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A set future
#[derive(Debug)]
pub struct Set<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) key: String,
pub(super) value: Result<Value>,
}
impl<'r, Client> IntoFuture for Set<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut conn = Client::new(Method::Set);
conn.execute(self.router?, Param::new(vec![self.key.into(), self.value?])).await
})
}
}

View file

@ -0,0 +1,41 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Error;
use crate::api::ExtraFeatures;
use crate::api::Result;
use crate::sql::Value;
use serde::de::DeserializeOwned;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// A signin future
#[derive(Debug)]
pub struct Signin<'r, C: Connection, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) credentials: Result<Value>,
pub(super) response_type: PhantomData<R>,
}
impl<'r, Client, R> IntoFuture for Signin<'r, Client, R>
where
Client: Connection,
R: DeserializeOwned + Send + Sync,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let router = self.router?;
if !router.features.contains(&ExtraFeatures::Auth) {
return Err(Error::AuthNotSupported.into());
}
let mut conn = Client::new(Method::Signin);
conn.execute(router, Param::new(vec![self.credentials?])).await
})
}
}

View file

@ -0,0 +1,41 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Error;
use crate::api::ExtraFeatures;
use crate::api::Result;
use crate::sql::Value;
use serde::de::DeserializeOwned;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// A signup future
#[derive(Debug)]
pub struct Signup<'r, C: Connection, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) credentials: Result<Value>,
pub(super) response_type: PhantomData<R>,
}
impl<'r, Client, R> IntoFuture for Signup<'r, Client, R>
where
Client: Connection,
R: DeserializeOwned + Send + Sync,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let router = self.router?;
if !router.features.contains(&ExtraFeatures::Auth) {
return Err(Error::AuthNotSupported.into());
}
let mut conn = Client::new(Method::Signup);
conn.execute(router, Param::new(vec![self.credentials?])).await
})
}
}

View file

@ -0,0 +1,181 @@
#![cfg(any(feature = "protocol-http", feature = "protocol-ws"))]
#![cfg(not(target_arch = "wasm32"))]
mod protocol;
mod server;
mod types;
use crate::api::method::tests::types::AuthParams;
use crate::api::opt::auth::Database;
use crate::api::opt::auth::Jwt;
use crate::api::opt::auth::Namespace;
use crate::api::opt::auth::Root;
use crate::api::opt::auth::Scope;
use crate::api::opt::PatchOp;
use crate::api::Response as QueryResponse;
use crate::api::Surreal;
use crate::sql::statements::BeginStatement;
use crate::sql::statements::CommitStatement;
use protocol::Client;
use protocol::Test;
use semver::Version;
use std::ops::Bound;
use types::User;
use types::USER;
static DB: Surreal<Client> = Surreal::init();
#[tokio::test]
async fn api() {
// connect to the mock server
DB.connect::<Test>(()).with_capacity(512).await.unwrap();
// health
let _: () = DB.health().await.unwrap();
// invalidate
let _: () = DB.invalidate().await.unwrap();
// use
let _: () = DB.use_ns("test-ns").use_db("test-db").await.unwrap();
// signup
let _: Jwt = DB
.signup(Scope {
namespace: "test-ns",
database: "test-db",
scope: "scope",
params: AuthParams {},
})
.await
.unwrap();
// signin
let _: () = DB
.signin(Root {
username: "root",
password: "root",
})
.await
.unwrap();
let _: Jwt = DB
.signin(Namespace {
namespace: "test-ns",
username: "user",
password: "pass",
})
.await
.unwrap();
let _: Jwt = DB
.signin(Database {
namespace: "test-ns",
database: "test-db",
username: "user",
password: "pass",
})
.await
.unwrap();
let _: Jwt = DB
.signin(Scope {
namespace: "test-ns",
database: "test-db",
scope: "scope",
params: AuthParams {},
})
.await
.unwrap();
// authenticate
let _: () = DB.authenticate(Jwt(String::new())).await.unwrap();
// query
let _: QueryResponse = DB.query("SELECT * FROM user").await.unwrap();
let _: QueryResponse =
DB.query("CREATE user:john SET name = $name").bind(("name", "John Doe")).await.unwrap();
let _: QueryResponse = DB
.query("CREATE user:john SET name = $name")
.bind(User {
id: "john".to_owned(),
name: "John Doe".to_owned(),
})
.await
.unwrap();
let _: QueryResponse = DB
.query(BeginStatement)
.query("CREATE account:one SET balance = 135605.16")
.query("CREATE account:two SET balance = 91031.31")
.query("UPDATE account:one SET balance += 300.00")
.query("UPDATE account:two SET balance -= 300.00")
.query(CommitStatement)
.await
.unwrap();
// create
let _: User = DB.create(USER).await.unwrap();
let _: User = DB.create((USER, "john")).await.unwrap();
let _: User = DB.create(USER).content(User::default()).await.unwrap();
let _: User = DB.create((USER, "john")).content(User::default()).await.unwrap();
// select
let _: Vec<User> = DB.select(USER).await.unwrap();
let _: Option<User> = DB.select((USER, "john")).await.unwrap();
let _: Vec<User> = DB.select(USER).range(..).await.unwrap();
let _: Vec<User> = DB.select(USER).range(.."john").await.unwrap();
let _: Vec<User> = DB.select(USER).range(..="john").await.unwrap();
let _: Vec<User> = DB.select(USER).range("jane"..).await.unwrap();
let _: Vec<User> = DB.select(USER).range("jane".."john").await.unwrap();
let _: Vec<User> = DB.select(USER).range("jane"..="john").await.unwrap();
let _: Vec<User> = DB.select(USER).range("jane"..="john").await.unwrap();
let _: Vec<User> =
DB.select(USER).range((Bound::Excluded("jane"), Bound::Included("john"))).await.unwrap();
// update
let _: Vec<User> = DB.update(USER).await.unwrap();
let _: Option<User> = DB.update((USER, "john")).await.unwrap();
let _: Vec<User> = DB.update(USER).content(User::default()).await.unwrap();
let _: Vec<User> =
DB.update(USER).range("jane".."john").content(User::default()).await.unwrap();
let _: Option<User> = DB.update((USER, "john")).content(User::default()).await.unwrap();
// merge
let _: Vec<User> = DB.update(USER).merge(User::default()).await.unwrap();
let _: Vec<User> = DB.update(USER).range("jane".."john").merge(User::default()).await.unwrap();
let _: Option<User> = DB.update((USER, "john")).merge(User::default()).await.unwrap();
// patch
let _: Vec<User> = DB.update(USER).patch(PatchOp::remove("/name")).await.unwrap();
let _: Vec<User> =
DB.update(USER).range("jane".."john").patch(PatchOp::remove("/name")).await.unwrap();
let _: Option<User> = DB.update((USER, "john")).patch(PatchOp::remove("/name")).await.unwrap();
// delete
let _: () = DB.delete(USER).await.unwrap();
let _: () = DB.delete((USER, "john")).await.unwrap();
let _: () = DB.delete(USER).range("jane".."john").await.unwrap();
// export
let _: () = DB.export("backup.sql").await.unwrap();
// import
let _: () = DB.import("backup.sql").await.unwrap();
// version
let _: Version = DB.version().await.unwrap();
}
fn send_and_sync(_: impl Send + Sync) {}
#[test]
fn futures_are_send_and_sync() {
send_and_sync(async {
let db = Surreal::new::<Test>(()).await.unwrap();
db.signin(Root {
username: "root",
password: "root",
})
.await
.unwrap();
db.use_ns("test-ns").use_db("test-db").await.unwrap();
let _: Vec<User> = db.select(USER).await.unwrap();
});
}

View file

@ -0,0 +1,144 @@
use super::server;
use crate::api::conn::Connection;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Route;
use crate::api::conn::Router;
use crate::api::opt::from_value;
use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::Connect;
use crate::api::ExtraFeatures;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::api::Surreal;
use flume::Receiver;
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
use url::Url;
#[derive(Debug)]
pub struct Test;
impl IntoEndpoint<Test> for () {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
Ok(Endpoint {
endpoint: Url::parse("test://")?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
#[derive(Debug, Clone)]
pub struct Client {
method: Method,
}
impl Surreal<Client> {
pub fn connect<P>(
&'static self,
address: impl IntoEndpoint<P, Client = Client>,
) -> Connect<Client, ()> {
Connect {
router: Some(&self.router),
address: address.into_endpoint(),
capacity: 0,
client: PhantomData,
response_type: PhantomData,
}
}
}
impl crate::api::Connection for Client {}
impl Connection for Client {
fn new(method: Method) -> Self {
Self {
method,
}
}
fn connect(
_address: Endpoint,
capacity: usize,
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
Box::pin(async move {
let (route_tx, route_rx) = flume::bounded(capacity);
let mut features = HashSet::new();
features.insert(ExtraFeatures::Auth);
features.insert(ExtraFeatures::Backup);
let router = Router {
features,
conn: PhantomData,
sender: route_tx,
last_id: AtomicI64::new(0),
};
server::mock(route_rx);
Ok(Surreal {
router: OnceCell::with_value(Arc::new(router)),
})
})
}
fn send<'r>(
&'r mut self,
router: &'r Router<Self>,
param: Param,
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
Box::pin(async move {
let (sender, receiver) = flume::bounded(1);
let route = Route {
request: (0, self.method, param),
response: sender,
};
router
.sender
.send_async(Some(route))
.await
.as_ref()
.map_err(ToString::to_string)
.unwrap();
Ok(receiver)
})
}
fn recv<R>(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
where
R: DeserializeOwned,
{
Box::pin(async move {
let result = rx.into_recv_async().await.unwrap();
match result.unwrap() {
DbResponse::Other(value) => from_value(value),
DbResponse::Query(..) => unreachable!(),
}
})
}
fn recv_query(
&mut self,
rx: Receiver<Result<DbResponse>>,
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
Box::pin(async move {
let result = rx.into_recv_async().await.unwrap();
match result.unwrap() {
DbResponse::Query(results) => Ok(results),
DbResponse::Other(..) => unreachable!(),
}
})
}
}

View file

@ -0,0 +1,106 @@
use super::types::Credentials;
use super::types::User;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Route;
use crate::api::opt::from_json;
use crate::api::opt::from_value;
use crate::api::Response as QueryResponse;
use crate::sql::Array;
use crate::sql::Value;
use flume::Receiver;
use futures::StreamExt;
use serde_json::json;
use std::mem;
pub(super) fn mock(route_rx: Receiver<Option<Route>>) {
tokio::spawn(async move {
let mut stream = route_rx.into_stream();
while let Some(Some(Route {
request,
response,
})) = stream.next().await
{
let (_, method, param) = request;
let mut params = param.other;
let result = match method {
Method::Invalidate | Method::Health => match &params[..] {
[] => Ok(DbResponse::Other(Value::None)),
_ => unreachable!(),
},
Method::Authenticate | Method::Kill | Method::Unset | Method::Delete => {
match &params[..] {
[_] => Ok(DbResponse::Other(Value::None)),
_ => unreachable!(),
}
}
Method::Live => match &params[..] {
[_] => Ok(DbResponse::Other(
"c6c0e36c-e2cf-42cb-b2d5-75415249b261".to_owned().into(),
)),
_ => unreachable!(),
},
Method::Version => match &params[..] {
[] => Ok(DbResponse::Other("1.0.0".into())),
_ => unreachable!(),
},
Method::Use => match &params[..] {
[_] | [_, _] => Ok(DbResponse::Other(Value::None)),
_ => unreachable!(),
},
Method::Signup | Method::Signin => match &mut params[..] {
[credentials] => {
let credentials: Credentials = from_value(mem::take(credentials)).unwrap();
match credentials {
Credentials::Root {
..
} => Ok(DbResponse::Other(Value::None)),
_ => Ok(DbResponse::Other("jwt".to_owned().into())),
}
}
_ => unreachable!(),
},
Method::Set => match &params[..] {
[_, _] => Ok(DbResponse::Other(Value::None)),
_ => unreachable!(),
},
Method::Query => match param.query {
Some(_) => Ok(DbResponse::Query(QueryResponse(Default::default()))),
_ => unreachable!(),
},
Method::Create => match &params[..] {
[_] => Ok(DbResponse::Other(from_json(json!(User::default())))),
[_, user] => Ok(DbResponse::Other(user.clone())),
_ => unreachable!(),
},
Method::Select => match &params[..] {
[Value::Thing(..)] => Ok(DbResponse::Other(from_json(json!(User::default())))),
[Value::Table(..) | Value::Array(..) | Value::Range(..)] => {
Ok(DbResponse::Other(Value::Array(Array(Vec::new()))))
}
_ => unreachable!(),
},
Method::Update | Method::Merge | Method::Patch => match &params[..] {
[Value::Thing(..)] | [Value::Thing(..), _] => {
Ok(DbResponse::Other(from_json(json!(User::default()))))
}
[Value::Table(..) | Value::Array(..) | Value::Range(..)]
| [Value::Table(..) | Value::Array(..) | Value::Range(..), _] => {
Ok(DbResponse::Other(Value::Array(Array(Vec::new()))))
}
_ => unreachable!(),
},
Method::Export | Method::Import => match param.file {
Some(_) => Ok(DbResponse::Other(Value::None)),
_ => unreachable!(),
},
};
if let Err(message) = response.into_send_async(result).await {
panic!("message dropped; {message:?}");
}
}
});
}

View file

@ -0,0 +1,38 @@
use serde::Deserialize;
use serde::Serialize;
pub const USER: &str = "user";
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Credentials {
Database {
ns: String,
db: String,
user: String,
pass: String,
},
Namespace {
ns: String,
user: String,
pass: String,
},
Root {
user: String,
pass: String,
},
Scope {
ns: String,
db: String,
sc: String,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthParams {}

View file

@ -0,0 +1,30 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Result;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// An unset future
#[derive(Debug)]
pub struct Unset<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) key: String,
}
impl<'r, Client> IntoFuture for Unset<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut conn = Client::new(Method::Unset);
conn.execute(self.router?, Param::new(vec![self.key.into()])).await
})
}
}

View file

@ -0,0 +1,135 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::method::Content;
use crate::api::method::Merge;
use crate::api::method::Patch;
use crate::api::opt::PatchOp;
use crate::api::opt::Range;
use crate::api::opt::Resource;
use crate::api::Connection;
use crate::api::Result;
use crate::sql::Id;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
/// An update future
#[derive(Debug)]
pub struct Update<'r, C: Connection, R> {
pub(super) router: Result<&'r Router<C>>,
pub(super) resource: Result<Resource>,
pub(super) range: Option<Range<Id>>,
pub(super) response_type: PhantomData<R>,
}
impl<'r, Client, R> Update<'r, Client, R>
where
Client: Connection,
{
async fn execute<T>(self) -> Result<T>
where
T: DeserializeOwned,
{
let resource = self.resource?;
let param = match self.range {
Some(range) => resource.with_range(range)?,
None => resource.into(),
};
let mut conn = Client::new(Method::Update);
conn.execute(self.router?, Param::new(vec![param])).await
}
}
impl<'r, Client, R> IntoFuture for Update<'r, Client, Option<R>>
where
Client: Connection,
R: DeserializeOwned + Send + Sync + 'r,
{
type Output = Result<R>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
impl<'r, Client, R> IntoFuture for Update<'r, Client, Vec<R>>
where
Client: Connection,
R: DeserializeOwned + Send + Sync + 'r,
{
type Output = Result<Vec<R>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
impl<C, R> Update<'_, C, Vec<R>>
where
C: Connection,
{
/// Restricts the records to update to those in the specified range
pub fn range(mut self, bounds: impl Into<Range<Id>>) -> Self {
self.range = Some(bounds.into());
self
}
}
macro_rules! update_methods {
($this:ty, $res:ty) => {
impl<'r, C, R> Update<'r, C, $this>
where
C: Connection,
R: DeserializeOwned + Send + Sync,
{
/// Replaces the current document / record data with the specified data
pub fn content<D>(self, data: D) -> Content<'r, C, D, $res>
where
D: Serialize,
{
Content {
router: self.router,
method: Method::Update,
resource: self.resource,
range: self.range,
content: data,
response_type: PhantomData,
}
}
/// Merges the current document / record data with the specified data
pub fn merge<D>(self, data: D) -> Merge<'r, C, D, $res>
where
D: Serialize,
{
Merge {
router: self.router,
resource: self.resource,
range: self.range,
content: data,
response_type: PhantomData,
}
}
/// Patches the current document / record data with the specified JSON Patch data
pub fn patch(self, PatchOp(patch): PatchOp) -> Patch<'r, C, $res> {
Patch {
router: self.router,
resource: self.resource,
range: self.range,
patches: vec![patch],
response_type: PhantomData,
}
}
}
};
}
update_methods!(Option<R>, R);
update_methods!(Vec<R>, Vec<R>);

View file

@ -0,0 +1,31 @@
use crate::api::conn::Method;
use std::future::Future;
use std::pin::Pin;
use crate::api::conn::Param;
use crate::api::Connection;
use crate::api::Result;
use crate::api::conn::Router;
use std::future::IntoFuture;
use crate::sql::Value;
#[derive(Debug)]
pub struct UseDb<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) db: String,
}
impl<'r, Client> IntoFuture for UseDb<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut conn = Client::new(Method::Use);
conn.execute(self.router?, Param::new(vec![Value::None, self.db.into()]))
.await
})
}
}

View file

@ -0,0 +1,52 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::Connection;
use crate::api::Result;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// Stores the namespace to use
#[derive(Debug)]
pub struct UseNs<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) ns: String,
}
/// A use NS and DB future
#[derive(Debug)]
pub struct UseNsDb<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
pub(super) ns: String,
pub(super) db: String,
}
impl<'r, C> UseNs<'r, C>
where
C: Connection,
{
/// Switch to a specific database
pub fn use_db(self, db: impl Into<String>) -> UseNsDb<'r, C> {
UseNsDb {
db: db.into(),
ns: self.ns,
router: self.router,
}
}
}
impl<'r, Client> IntoFuture for UseNsDb<'r, Client>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut conn = Client::new(Method::Use);
conn.execute(self.router?, Param::new(vec![self.ns.into(), self.db.into()])).await
})
}
}

View file

@ -0,0 +1,32 @@
use crate::api::conn::Method;
use crate::api::conn::Param;
use crate::api::conn::Router;
use crate::api::err::Error;
use crate::api::Connection;
use crate::api::Result;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A version future
#[derive(Debug)]
pub struct Version<'r, C: Connection> {
pub(super) router: Result<&'r Router<C>>,
}
impl<'r, Client> IntoFuture for Version<'r, Client>
where
Client: Connection,
{
type Output = Result<semver::Version>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async {
let mut conn = Client::new(Method::Version);
let version: String = conn.execute(self.router?, Param::new(Vec::new())).await?;
let semantic = version.trim_start_matches("surrealdb-");
semantic.parse().map_err(|_| Error::InvalidSemanticVersion(semantic.to_string()).into())
})
}
}

192
lib/src/api/mod.rs Normal file
View file

@ -0,0 +1,192 @@
//! Functionality for connecting to local and remote databases
pub mod engines;
pub mod err;
pub mod method;
pub mod opt;
mod conn;
pub use method::query::Response;
use crate::api::conn::DbResponse;
use crate::api::conn::Router;
use crate::api::err::Error;
use crate::api::opt::Endpoint;
use once_cell::sync::OnceCell;
use semver::BuildMetadata;
use semver::VersionReq;
use std::fmt::Debug;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
/// A specialized `Result` type
pub type Result<T> = std::result::Result<T, crate::Error>;
const SUPPORTED_VERSIONS: (&str, &str) = (">=1.0.0-beta.8, <2.0.0", "20221030.c12a1cc");
const LOG: &str = "surrealdb::api";
/// Connection trait implemented by supported engines
pub trait Connection: conn::Connection {}
/// The future returned when creating a new SurrealDB instance
#[derive(Debug)]
pub struct Connect<'r, C: Connection, Response> {
router: Option<&'r OnceCell<Arc<Router<C>>>>,
address: Result<Endpoint>,
capacity: usize,
client: PhantomData<C>,
response_type: PhantomData<Response>,
}
impl<C, R> Connect<'_, C, R>
where
C: Connection,
{
/// Sets the maximum capacity of the connection
///
/// This is used to set bounds of the channels used internally
/// as well set the capacity of the `HashMap` used for routing
/// responses in case of the WebSocket client.
///
/// Setting this capacity to `0` (the default) means that
/// unbounded channels will be used. If your queries per second
/// are so high that the client is running out of memory,
/// it might be helpful to set this to a number that works best
/// for you.
///
/// # Examples
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> surrealdb::Result<()> {
/// use surrealdb::engines::remote::ws::Ws;
/// use surrealdb::Surreal;
///
/// let db = Surreal::new::<Ws>("localhost:8000")
/// .with_capacity(100_000)
/// .await?;
/// # Ok(())
/// # }
/// ```
#[must_use]
pub const fn with_capacity(mut self, capacity: usize) -> Self {
self.capacity = capacity;
self
}
}
impl<'r, Client> IntoFuture for Connect<'r, Client, Surreal<Client>>
where
Client: Connection,
{
type Output = Result<Surreal<Client>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let client = Client::connect(self.address?, self.capacity).await?;
client.check_server_version();
Ok(client)
})
}
}
impl<'r, Client> IntoFuture for Connect<'r, Client, ()>
where
Client: Connection,
{
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
match self.router {
Some(router) => {
let option =
Client::connect(self.address?, self.capacity).await?.router.into_inner();
match option {
Some(client) => {
let _res = router.set(client);
}
None => unreachable!(),
}
}
None => unreachable!(),
}
Ok(())
})
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) enum ExtraFeatures {
Auth,
Backup,
}
/// A database client instance for embedded or remote databases
#[derive(Debug)]
pub struct Surreal<C: Connection> {
router: OnceCell<Arc<Router<C>>>,
}
impl<C> Surreal<C>
where
C: Connection,
{
fn check_server_version(&self) {
let conn = self.clone();
tokio::spawn(async move {
let (versions, build_meta) = SUPPORTED_VERSIONS;
// invalid version requirements should be caught during development
let req = VersionReq::parse(versions).expect("valid supported versions");
let build_meta =
BuildMetadata::new(build_meta).expect("valid supported build metadata");
match conn.version().await {
Ok(version) => {
let server_build = &version.build;
if !req.matches(&version) {
warn!(target: LOG, "server version `{version}` does not match the range supported by the client `{versions}`");
} else if !server_build.is_empty() && server_build < &build_meta {
warn!(target: LOG, "server build `{server_build}` is older than the minimum supported build `{build_meta}`");
}
}
Err(error) => {
trace!(target: LOG, "failed to lookup the server version; {error:?}");
}
}
});
}
}
impl<C> Clone for Surreal<C>
where
C: Connection,
{
fn clone(&self) -> Self {
Self {
router: self.router.clone(),
}
}
}
trait ExtractRouter<C>
where
C: Connection,
{
fn extract(&self) -> Result<&Router<C>>;
}
impl<C> ExtractRouter<C> for OnceCell<Arc<Router<C>>>
where
C: Connection,
{
fn extract(&self) -> Result<&Router<C>> {
let router = self.get().ok_or(Error::ConnectionUninitialised)?;
Ok(router)
}
}

118
lib/src/api/opt/auth.rs Normal file
View file

@ -0,0 +1,118 @@
//! Authentication types
use crate::sql::Value;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
/// A signup action
#[derive(Debug)]
pub struct Signup;
/// A signin action
#[derive(Debug)]
pub struct Signin;
/// Credentials for authenticating with the server
pub trait Credentials<Action, Response>: Serialize {}
/// Credentials for the root user
#[derive(Debug, Serialize)]
pub struct Root<'a> {
/// The username of the root user
#[serde(rename = "user")]
pub username: &'a str,
/// The password of the root user
#[serde(rename = "pass")]
pub password: &'a str,
}
impl Credentials<Signin, ()> for Root<'_> {}
/// Credentials for the namespace user
#[derive(Debug, Serialize)]
pub struct Namespace<'a> {
/// The namespace the user has access to
#[serde(rename = "ns")]
pub namespace: &'a str,
/// The username of the namespace user
#[serde(rename = "user")]
pub username: &'a str,
/// The password of the namespace user
#[serde(rename = "pass")]
pub password: &'a str,
}
impl Credentials<Signin, Jwt> for Namespace<'_> {}
/// Credentials for the database user
#[derive(Debug, Serialize)]
pub struct Database<'a> {
/// The namespace the user has access to
#[serde(rename = "ns")]
pub namespace: &'a str,
/// The database the user has access to
#[serde(rename = "db")]
pub database: &'a str,
/// The username of the database user
#[serde(rename = "user")]
pub username: &'a str,
/// The password of the database user
#[serde(rename = "pass")]
pub password: &'a str,
}
impl Credentials<Signin, Jwt> for Database<'_> {}
/// Credentials for the scope user
#[derive(Debug, Serialize)]
pub struct Scope<'a, P> {
/// The namespace the user has access to
#[serde(rename = "ns")]
pub namespace: &'a str,
/// The database the user has access to
#[serde(rename = "db")]
pub database: &'a str,
/// The scope to use for signin and signup
#[serde(rename = "sc")]
pub scope: &'a str,
/// The additional params to use
#[serde(flatten)]
pub params: P,
}
impl<T, P> Credentials<T, Jwt> for Scope<'_, P> where P: Serialize {}
/// A JSON Web Token for authenticating with the server
#[derive(Clone, Serialize, Deserialize)]
pub struct Jwt(pub(crate) String);
impl From<String> for Jwt {
fn from(jwt: String) -> Self {
Jwt(jwt)
}
}
impl<'a> From<&'a String> for Jwt {
fn from(jwt: &'a String) -> Self {
Jwt(jwt.to_owned())
}
}
impl<'a> From<&'a str> for Jwt {
fn from(jwt: &'a str) -> Self {
Jwt(jwt.to_owned())
}
}
impl From<Jwt> for Value {
fn from(Jwt(jwt): Jwt) -> Self {
jwt.into()
}
}
impl fmt::Debug for Jwt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Jwt(REDUCTED)")
}
}

View file

@ -0,0 +1,54 @@
use crate::api::engines::local::Db;
use crate::api::engines::local::FDb;
use crate::api::err::Error;
use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use std::path::Path;
use url::Url;
impl IntoEndpoint<FDb> for &str {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("fdb://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<FDb> for &Path {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("fdb://{}", self.display());
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl<T> IntoEndpoint<FDb> for (T, Strict)
where
T: AsRef<Path>,
{
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("fdb://{}", self.0.as_ref().display());
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: true,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}

View file

@ -0,0 +1,127 @@
use crate::api::engines::remote::http::Client;
use crate::api::engines::remote::http::Http;
use crate::api::engines::remote::http::Https;
use crate::api::err::Error;
use crate::api::opt::IntoEndpoint;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use crate::api::opt::Tls;
use crate::api::Endpoint;
use crate::api::Result;
use std::net::SocketAddr;
use url::Url;
impl IntoEndpoint<Http> for &str {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("http://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Http> for SocketAddr {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("http://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Http> for String {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("http://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Https> for &str {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("https://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Https> for SocketAddr {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("https://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Https> for String {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("https://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
impl<T> IntoEndpoint<Https> for (T, native_tls::TlsConnector)
where
T: IntoEndpoint<Https>,
{
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let (address, config) = self;
let mut address = address.into_endpoint()?;
address.tls_config = Some(Tls::Native(config));
Ok(address)
}
}
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
impl<T> IntoEndpoint<Https> for (T, rustls::ClientConfig)
where
T: IntoEndpoint<Https>,
{
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let (address, config) = self;
let mut address = address.into_endpoint()?;
address.tls_config = Some(Tls::Rust(config));
Ok(address)
}
}

View file

@ -0,0 +1,32 @@
use crate::api::engines::local::Db;
use crate::api::engines::local::IndxDb;
use crate::api::err::Error;
use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use url::Url;
impl IntoEndpoint<IndxDb> for &str {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("indxdb://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<IndxDb> for (&str, Strict) {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let mut address = IntoEndpoint::<IndxDb>::into_endpoint(self.0)?;
address.strict = true;
Ok(address)
}
}

View file

@ -0,0 +1,30 @@
use crate::api::engines::local::Db;
use crate::api::engines::local::Mem;
use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use url::Url;
impl IntoEndpoint<Mem> for () {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
Ok(Endpoint {
endpoint: Url::parse("mem://").unwrap(),
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Mem> for Strict {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let mut address = IntoEndpoint::<Mem>::into_endpoint(())?;
address.strict = true;
Ok(address)
}
}

View file

@ -0,0 +1,38 @@
#[cfg(feature = "protocol-http")]
mod http;
#[cfg(feature = "protocol-ws")]
mod ws;
#[cfg(feature = "kv-fdb")]
mod fdb;
#[cfg(feature = "kv-indxdb")]
mod indxdb;
#[cfg(feature = "kv-mem")]
mod mem;
#[cfg(feature = "kv-rocksdb")]
mod rocksdb;
#[cfg(feature = "kv-tikv")]
mod tikv;
use crate::api::Connection;
use crate::api::Result;
use url::Url;
/// A server address used to connect to the server
#[derive(Debug)]
#[allow(dead_code)] // used by the embedded and remote connections
pub struct Endpoint {
pub(crate) endpoint: Url,
#[allow(dead_code)] // used by the embedded database
pub(crate) strict: bool,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
pub(crate) tls_config: Option<super::Tls>,
}
/// A trait for converting inputs to a server address object
pub trait IntoEndpoint<Scheme> {
/// The client implied by this scheme and address combination
type Client: Connection;
/// Converts an input into a server address object
fn into_endpoint(self) -> Result<Endpoint>;
}

View file

@ -0,0 +1,100 @@
use crate::api::engines::local::Db;
use crate::api::engines::local::File;
use crate::api::engines::local::RocksDb;
use crate::api::err::Error;
use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use std::path::Path;
use url::Url;
impl IntoEndpoint<RocksDb> for &str {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("rocksdb://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<RocksDb> for &Path {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("rocksdb://{}", self.display());
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl<T> IntoEndpoint<RocksDb> for (T, Strict)
where
T: AsRef<Path>,
{
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("rocksdb://{}", self.0.as_ref().display());
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: true,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<File> for &str {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("file://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<File> for &Path {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("file://{}", self.display());
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl<T> IntoEndpoint<File> for (T, Strict)
where
T: AsRef<Path>,
{
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("file://{}", self.0.as_ref().display());
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: true,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}

View file

@ -0,0 +1,64 @@
use crate::api::engines::local::Db;
use crate::api::engines::local::TiKv;
use crate::api::err::Error;
use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use std::net::SocketAddr;
use url::Url;
impl IntoEndpoint<TiKv> for &str {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("tikv://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<TiKv> for SocketAddr {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("tikv://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<TiKv> for String {
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("tikv://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl<T> IntoEndpoint<TiKv> for (T, Strict)
where
T: IntoEndpoint<TiKv>,
{
type Client = Db;
fn into_endpoint(self) -> Result<Endpoint> {
let mut address = self.0.into_endpoint()?;
address.strict = true;
Ok(address)
}
}

View file

@ -0,0 +1,127 @@
use crate::api::engines::remote::ws::Client;
use crate::api::engines::remote::ws::Ws;
use crate::api::engines::remote::ws::Wss;
use crate::api::err::Error;
use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use crate::api::opt::Tls;
use crate::api::Result;
use std::net::SocketAddr;
use url::Url;
impl IntoEndpoint<Ws> for &str {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("ws://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Ws> for SocketAddr {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("ws://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Ws> for String {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("ws://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Wss> for &str {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("wss://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Wss> for SocketAddr {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("wss://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
impl IntoEndpoint<Wss> for String {
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let url = format!("wss://{self}");
Ok(Endpoint {
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
strict: false,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: None,
})
}
}
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
impl<T> IntoEndpoint<Wss> for (T, native_tls::TlsConnector)
where
T: IntoEndpoint<Wss>,
{
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let (address, config) = self;
let mut address = address.into_endpoint()?;
address.tls_config = Some(Tls::Native(config));
Ok(address)
}
}
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
impl<T> IntoEndpoint<Wss> for (T, rustls::ClientConfig)
where
T: IntoEndpoint<Wss>,
{
type Client = Client;
fn into_endpoint(self) -> Result<Endpoint> {
let (address, config) = self;
let mut address = address.into_endpoint()?;
address.tls_config = Some(Tls::Rust(config));
Ok(address)
}
}

197
lib/src/api/opt/mod.rs Normal file
View file

@ -0,0 +1,197 @@
//! The different options and types for use in API functions
pub mod auth;
mod endpoint;
mod query;
mod resource;
mod strict;
mod tls;
use crate::api::err::Error;
use crate::api::Result;
use crate::sql;
use crate::sql::Thing;
use crate::sql::Value;
use dmp::Diff;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::json;
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
pub use endpoint::*;
pub use query::*;
pub use resource::*;
pub use strict::*;
pub use tls::*;
/// Record ID
pub type RecordId = Thing;
type UnitOp<'a> = InnerOp<'a, ()>;
#[derive(Debug, Serialize)]
#[serde(tag = "op", rename_all = "lowercase")]
enum InnerOp<'a, T> {
Add {
path: &'a str,
value: T,
},
Remove {
path: &'a str,
},
Replace {
path: &'a str,
value: T,
},
Change {
path: &'a str,
value: String,
},
}
/// A [JSON Patch] operation
///
/// From the official website:
///
/// > JSON Patch is a format for describing changes to a JSON document.
/// > It can be used to avoid sending a whole document when only a part has changed.
///
/// [JSON Patch]: https://jsonpatch.com/
#[derive(Debug)]
pub struct PatchOp(pub(crate) Value);
impl PatchOp {
/// Adds a value to an object or inserts it into an array.
///
/// In the case of an array, the value is inserted before the given index.
/// The `-` character can be used instead of an index to insert at the end of an array.
///
/// # Examples
///
/// ```
/// # use serde_json::json;
/// # use surrealdb::opt::PatchOp;
/// PatchOp::add("/biscuits/1", json!({ "name": "Ginger Nut" }))
/// # ;
/// ```
#[must_use]
pub fn add<T>(path: &str, value: T) -> Self
where
T: Serialize,
{
let value = from_json(json!(InnerOp::Add {
path,
value
}));
Self(value)
}
/// Removes a value from an object or array.
///
/// # Examples
///
/// ```
/// # use surrealdb::opt::PatchOp;
/// PatchOp::remove("/biscuits")
/// # ;
/// ```
///
/// Remove the first element of the array at `biscuits`
/// (or just removes the “0” key if `biscuits` is an object)
///
/// ```
/// # use surrealdb::opt::PatchOp;
/// PatchOp::remove("/biscuits/0")
/// # ;
/// ```
#[must_use]
pub fn remove(path: &str) -> Self {
let value = from_json(json!(UnitOp::Remove {
path
}));
Self(value)
}
/// Replaces a value.
///
/// Equivalent to a “remove” followed by an “add”.
///
/// # Examples
///
/// ```
/// # use surrealdb::opt::PatchOp;
/// PatchOp::replace("/biscuits/0/name", "Chocolate Digestive")
/// # ;
/// ```
#[must_use]
pub fn replace<T>(path: &str, value: T) -> Self
where
T: Serialize,
{
let value = from_json(json!(InnerOp::Replace {
path,
value
}));
Self(value)
}
/// Changes a value
#[must_use]
pub fn change(path: &str, diff: Diff) -> Self {
let value = from_json(json!(UnitOp::Change {
path,
value: diff.text,
}));
Self(value)
}
}
/// Deserializes a value `T` from `SurrealDB` [`Value`]
pub(crate) fn from_value<T>(value: sql::Value) -> Result<T>
where
T: DeserializeOwned,
{
let bytes = match msgpack::to_vec(&value) {
Ok(bytes) => bytes,
Err(error) => {
return Err(Error::FromValue {
value,
error: error.to_string(),
}
.into());
}
};
match msgpack::from_slice(&bytes) {
Ok(response) => Ok(response),
Err(error) => Err(Error::FromValue {
value,
error: error.to_string(),
}
.into()),
}
}
pub(crate) fn from_json(json: JsonValue) -> sql::Value {
match json {
JsonValue::Null => sql::Value::None,
JsonValue::Bool(boolean) => boolean.into(),
JsonValue::Number(number) => match (number.as_u64(), number.as_i64(), number.as_f64()) {
(Some(number), _, _) => number.into(),
(_, Some(number), _) => number.into(),
(_, _, Some(number)) => number.into(),
_ => unreachable!(),
},
JsonValue::String(string) => match sql::thing(&string) {
Ok(thing) => thing.into(),
Err(_) => string.into(),
},
JsonValue::Array(array) => array.into_iter().map(from_json).collect::<Vec<_>>().into(),
JsonValue::Object(object) => object
.into_iter()
.map(|(key, value)| (key, from_json(value)))
.collect::<BTreeMap<_, _>>()
.into(),
}
}

329
lib/src/api/opt/query.rs Normal file
View file

@ -0,0 +1,329 @@
use crate::api::err::Error;
use crate::api::opt::from_value;
use crate::api::Response as QueryResponse;
use crate::api::Result;
use crate::sql;
use crate::sql::statements::*;
use crate::sql::Object;
use crate::sql::Statement;
use crate::sql::Statements;
use crate::sql::Value;
use serde::de::DeserializeOwned;
use std::mem;
/// A trait for converting inputs into SQL statements
pub trait IntoQuery {
/// Converts an input into SQL statements
fn into_query(self) -> Result<Vec<Statement>>;
}
impl IntoQuery for sql::Query {
fn into_query(self) -> Result<Vec<Statement>> {
let sql::Query(Statements(statements)) = self;
Ok(statements)
}
}
impl IntoQuery for Statements {
fn into_query(self) -> Result<Vec<Statement>> {
let Statements(statements) = self;
Ok(statements)
}
}
impl IntoQuery for Vec<Statement> {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(self)
}
}
impl IntoQuery for Statement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![self])
}
}
impl IntoQuery for UseStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Use(self)])
}
}
impl IntoQuery for SetStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Set(self)])
}
}
impl IntoQuery for InfoStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Info(self)])
}
}
impl IntoQuery for LiveStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Live(self)])
}
}
impl IntoQuery for KillStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Kill(self)])
}
}
impl IntoQuery for BeginStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Begin(self)])
}
}
impl IntoQuery for CancelStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Cancel(self)])
}
}
impl IntoQuery for CommitStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Commit(self)])
}
}
impl IntoQuery for OutputStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Output(self)])
}
}
impl IntoQuery for IfelseStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Ifelse(self)])
}
}
impl IntoQuery for SelectStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Select(self)])
}
}
impl IntoQuery for CreateStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Create(self)])
}
}
impl IntoQuery for UpdateStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Update(self)])
}
}
impl IntoQuery for RelateStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Relate(self)])
}
}
impl IntoQuery for DeleteStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Delete(self)])
}
}
impl IntoQuery for InsertStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Insert(self)])
}
}
impl IntoQuery for DefineStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Define(self)])
}
}
impl IntoQuery for RemoveStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Remove(self)])
}
}
impl IntoQuery for OptionStatement {
fn into_query(self) -> Result<Vec<Statement>> {
Ok(vec![Statement::Option(self)])
}
}
impl IntoQuery for &str {
fn into_query(self) -> Result<Vec<Statement>> {
sql::parse(self)?.into_query()
}
}
impl IntoQuery for &String {
fn into_query(self) -> Result<Vec<Statement>> {
sql::parse(self)?.into_query()
}
}
impl IntoQuery for String {
fn into_query(self) -> Result<Vec<Statement>> {
sql::parse(&self)?.into_query()
}
}
/// Represents a way to take a single query result from a list of responses
pub trait QueryResult<Response>
where
Response: DeserializeOwned,
{
/// Extracts and deserializes a query result from a query response
fn query_result(self, response: &mut QueryResponse) -> Result<Response>;
}
impl<T> QueryResult<Option<T>> for usize
where
T: DeserializeOwned,
{
fn query_result(self, QueryResponse(map): &mut QueryResponse) -> Result<Option<T>> {
let vec = match map.get_mut(&self) {
Some(result) => match result {
Ok(vec) => vec,
Err(error) => {
let error = mem::replace(error, Error::ConnectionUninitialised.into());
map.remove(&self);
return Err(error);
}
},
None => {
return Ok(None);
}
};
let result = match &mut vec[..] {
[] => Ok(None),
[value] => {
let value = mem::take(value);
from_value(value)
}
_ => Err(Error::LossyTake(QueryResponse(mem::take(map))).into()),
};
map.remove(&self);
result
}
}
impl<T> QueryResult<Option<T>> for (usize, &str)
where
T: DeserializeOwned,
{
fn query_result(self, QueryResponse(map): &mut QueryResponse) -> Result<Option<T>> {
let (index, key) = self;
let vec = match map.get_mut(&index) {
Some(result) => match result {
Ok(vec) => vec,
Err(error) => {
let error = mem::replace(error, Error::ConnectionUninitialised.into());
map.remove(&index);
return Err(error);
}
},
None => {
return Ok(None);
}
};
let mut value = match &mut vec[..] {
[] => {
map.remove(&index);
return Ok(None);
}
[value] => value,
_ => {
return Err(Error::LossyTake(QueryResponse(mem::take(map))).into());
}
};
match &mut value {
Value::None | Value::Null => {
map.remove(&index);
Ok(None)
}
Value::Object(Object(object)) => {
if object.is_empty() {
map.remove(&index);
return Ok(None);
}
let Some(value) = object.remove(key) else {
return Ok(None);
};
from_value(value)
}
_ => Ok(None),
}
}
}
impl<T> QueryResult<Vec<T>> for usize
where
T: DeserializeOwned,
{
fn query_result(self, QueryResponse(map): &mut QueryResponse) -> Result<Vec<T>> {
let vec = match map.remove(&self) {
Some(result) => result?,
None => {
return Ok(vec![]);
}
};
from_value(vec.into())
}
}
impl<T> QueryResult<Vec<T>> for (usize, &str)
where
T: DeserializeOwned,
{
fn query_result(self, QueryResponse(map): &mut QueryResponse) -> Result<Vec<T>> {
let (index, key) = self;
let response = match map.get_mut(&index) {
Some(result) => match result {
Ok(vec) => vec,
Err(error) => {
let error = mem::replace(error, Error::ConnectionUninitialised.into());
map.remove(&index);
return Err(error);
}
},
None => {
return Ok(vec![]);
}
};
let mut vec = Vec::with_capacity(response.len());
for value in response.iter_mut() {
if let Value::Object(Object(object)) = value {
if let Some(value) = object.remove(key) {
vec.push(value);
}
}
}
from_value(vec.into())
}
}
impl<T> QueryResult<Option<T>> for &str
where
T: DeserializeOwned,
{
fn query_result(self, response: &mut QueryResponse) -> Result<Option<T>> {
(0, self).query_result(response)
}
}
impl<T> QueryResult<Vec<T>> for &str
where
T: DeserializeOwned,
{
fn query_result(self, response: &mut QueryResponse) -> Result<Vec<T>> {
(0, self).query_result(response)
}
}

257
lib/src/api/opt/resource.rs Normal file
View file

@ -0,0 +1,257 @@
use crate::api::err::Error;
use crate::api::Result;
use crate::sql;
use crate::sql::Array;
use crate::sql::Edges;
use crate::sql::Id;
use crate::sql::Object;
use crate::sql::Table;
use crate::sql::Thing;
use crate::sql::Value;
use serde::Serialize;
use std::ops;
use std::ops::Bound;
/// A database resource
#[derive(Serialize)]
#[serde(untagged)]
#[derive(Debug)]
pub enum Resource {
/// Table name
Table(Table),
/// Record ID
RecordId(Thing),
/// An object
Object(Object),
/// An array
Array(Array),
/// Edges
Edges(Edges),
}
impl Resource {
pub(crate) fn with_range(self, range: Range<Id>) -> Result<Value> {
match self {
Resource::Table(Table(table)) => Ok(sql::Range {
tb: table,
beg: range.start,
end: range.end,
}
.into()),
Resource::RecordId(record_id) => Err(Error::RangeOnRecordId(record_id).into()),
Resource::Object(object) => Err(Error::RangeOnObject(object).into()),
Resource::Array(array) => Err(Error::RangeOnArray(array).into()),
Resource::Edges(edges) => Err(Error::RangeOnEdges(edges).into()),
}
}
}
impl From<Resource> for Value {
fn from(resource: Resource) -> Self {
match resource {
Resource::Table(resource) => resource.into(),
Resource::RecordId(resource) => resource.into(),
Resource::Object(resource) => resource.into(),
Resource::Array(resource) => resource.into(),
Resource::Edges(resource) => resource.into(),
}
}
}
/// A trait for converting inputs into database resources
pub trait IntoResource<Response>: Sized {
/// Converts an input into a database resource
fn into_resource(self) -> Result<Resource>;
}
impl<R> IntoResource<Option<R>> for Object {
fn into_resource(self) -> Result<Resource> {
Ok(Resource::Object(self))
}
}
impl<R> IntoResource<Option<R>> for Thing {
fn into_resource(self) -> Result<Resource> {
Ok(Resource::RecordId(self))
}
}
impl<R, T, I> IntoResource<Option<R>> for (T, I)
where
T: Into<String>,
I: Into<Id>,
{
fn into_resource(self) -> Result<Resource> {
let (table, id) = self;
let record_id = (table.into(), id.into());
Ok(Resource::RecordId(record_id.into()))
}
}
impl<R> IntoResource<Vec<R>> for Array {
fn into_resource(self) -> Result<Resource> {
Ok(Resource::Array(self))
}
}
impl<R> IntoResource<Vec<R>> for Edges {
fn into_resource(self) -> Result<Resource> {
Ok(Resource::Edges(self))
}
}
impl<R> IntoResource<Vec<R>> for Table {
fn into_resource(self) -> Result<Resource> {
Ok(Resource::Table(self))
}
}
fn blacklist_colon(input: &str) -> Result<()> {
match input.contains(':') {
true => {
// We already know this string contains a colon
let (table, id) = input.split_once(':').unwrap();
Err(Error::TableColonId {
table: table.to_owned(),
id: id.to_owned(),
}
.into())
}
false => Ok(()),
}
}
impl<R> IntoResource<Vec<R>> for &str {
fn into_resource(self) -> Result<Resource> {
blacklist_colon(self)?;
Ok(Resource::Table(Table(self.to_owned())))
}
}
impl<R> IntoResource<Vec<R>> for &String {
fn into_resource(self) -> Result<Resource> {
blacklist_colon(self)?;
Ok(Resource::Table(Table(self.to_owned())))
}
}
impl<R> IntoResource<Vec<R>> for String {
fn into_resource(self) -> Result<Resource> {
blacklist_colon(&self)?;
Ok(Resource::Table(Table(self)))
}
}
/// Holds the `start` and `end` bounds of a range query
#[derive(Debug)]
pub struct Range<T> {
pub(crate) start: Bound<T>,
pub(crate) end: Bound<T>,
}
impl<T> From<(Bound<T>, Bound<T>)> for Range<Id>
where
T: Into<Id>,
{
fn from((start, end): (Bound<T>, Bound<T>)) -> Self {
Self {
start: match start {
Bound::Included(idx) => Bound::Included(idx.into()),
Bound::Excluded(idx) => Bound::Excluded(idx.into()),
Bound::Unbounded => Bound::Unbounded,
},
end: match end {
Bound::Included(idx) => Bound::Included(idx.into()),
Bound::Excluded(idx) => Bound::Excluded(idx.into()),
Bound::Unbounded => Bound::Unbounded,
},
}
}
}
impl<T> From<ops::Range<T>> for Range<Id>
where
T: Into<Id>,
{
fn from(
ops::Range {
start,
end,
}: ops::Range<T>,
) -> Self {
Self {
start: Bound::Included(start.into()),
end: Bound::Excluded(end.into()),
}
}
}
impl<T> From<ops::RangeInclusive<T>> for Range<Id>
where
T: Into<Id>,
{
fn from(range: ops::RangeInclusive<T>) -> Self {
let (start, end) = range.into_inner();
Self {
start: Bound::Included(start.into()),
end: Bound::Included(end.into()),
}
}
}
impl<T> From<ops::RangeFrom<T>> for Range<Id>
where
T: Into<Id>,
{
fn from(
ops::RangeFrom {
start,
}: ops::RangeFrom<T>,
) -> Self {
Self {
start: Bound::Included(start.into()),
end: Bound::Unbounded,
}
}
}
impl<T> From<ops::RangeTo<T>> for Range<Id>
where
T: Into<Id>,
{
fn from(
ops::RangeTo {
end,
}: ops::RangeTo<T>,
) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Excluded(end.into()),
}
}
}
impl<T> From<ops::RangeToInclusive<T>> for Range<Id>
where
T: Into<Id>,
{
fn from(
ops::RangeToInclusive {
end,
}: ops::RangeToInclusive<T>,
) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Included(end.into()),
}
}
}
impl From<ops::RangeFull> for Range<Id> {
fn from(_: ops::RangeFull) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Unbounded,
}
}
}

10
lib/src/api/opt/strict.rs Normal file
View file

@ -0,0 +1,10 @@
/// Enables `strict` server mode
#[cfg(any(
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-fdb",
feature = "kv-indxdb",
))]
#[derive(Debug)]
pub struct Strict;

14
lib/src/api/opt/tls.rs Normal file
View file

@ -0,0 +1,14 @@
/// TLS Configuration
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls"))))]
#[derive(Debug)]
pub enum Tls {
/// Native TLS configuration
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
Native(native_tls::TlsConnector),
/// Rustls configuration
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
Rust(rustls::ClientConfig),
}

View file

@ -1,4 +1,5 @@
#[cfg(feature = "parallel")]
#[allow(dead_code)]
/// Specifies how many concurrent jobs can be buffered in the worker channel.
pub const MAX_CONCURRENT_TASKS: usize = 64;

View file

@ -14,6 +14,7 @@ use channel::Sender;
use std::ops::Bound;
impl Iterable {
#[allow(dead_code)]
pub(crate) async fn channel(
self,
ctx: &Context<'_>,

View file

@ -17,7 +17,7 @@ use futures::lock::Mutex;
use std::sync::Arc;
use trice::Instant;
pub struct Executor<'a> {
pub(crate) struct Executor<'a> {
err: bool,
kvs: &'a Datastore,
txn: Option<Transaction>,

View file

@ -18,7 +18,7 @@ use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::mem;
pub enum Iterable {
pub(crate) enum Iterable {
Value(Value),
Table(Table),
Thing(Thing),
@ -28,20 +28,20 @@ pub enum Iterable {
Relatable(Thing, Thing, Thing),
}
pub enum Operable {
pub(crate) enum Operable {
Value(Value),
Mergeable(Value, Value),
Relatable(Thing, Value, Thing),
}
pub enum Workable {
pub(crate) enum Workable {
Normal,
Insert(Value),
Relate(Thing, Thing),
}
#[derive(Default)]
pub struct Iterator {
pub(crate) struct Iterator {
// Iterator status
run: Canceller,
// Iterator limit value

View file

@ -10,14 +10,15 @@ mod transaction;
mod variables;
pub use self::auth::*;
pub use self::executor::*;
pub use self::iterator::*;
pub use self::options::*;
pub use self::response::*;
pub use self::session::*;
pub use self::statement::*;
pub use self::transaction::*;
pub use self::variables::*;
pub(crate) use self::executor::*;
pub(crate) use self::iterator::*;
pub(crate) use self::statement::*;
pub(crate) use self::transaction::*;
pub(crate) use self::variables::*;
#[cfg(feature = "parallel")]
mod channel;
@ -28,4 +29,4 @@ pub use self::channel::*;
#[cfg(test)]
pub(crate) mod test;
pub const LOG: &str = "surrealdb::dbs";
pub(crate) const LOG: &str = "surrealdb::dbs";

View file

@ -14,11 +14,10 @@ use crate::sql::statements::insert::InsertStatement;
use crate::sql::statements::relate::RelateStatement;
use crate::sql::statements::select::SelectStatement;
use crate::sql::statements::update::UpdateStatement;
use crate::sql::version::Version;
use std::fmt;
#[derive(Clone, Debug)]
pub enum Statement<'a> {
pub(crate) enum Statement<'a> {
Select(&'a SelectStatement),
Create(&'a CreateStatement),
Update(&'a UpdateStatement),
@ -164,14 +163,6 @@ impl<'a> Statement<'a> {
_ => None,
}
}
/// Returns any VERSION clause if specified
#[inline]
pub fn version(&self) -> Option<&Version> {
match self {
Statement::Select(v) => v.version.as_ref(),
_ => None,
}
}
/// Returns any RETURN clause if specified
#[inline]
pub fn output(&self) -> Option<&Output> {
@ -186,6 +177,7 @@ impl<'a> Statement<'a> {
}
/// Returns any RETURN clause if specified
#[inline]
#[allow(dead_code)]
pub fn parallel(&self) -> bool {
match self {
Statement::Select(v) => v.parallel,

View file

@ -2,4 +2,4 @@ use crate::kvs;
use futures::lock::Mutex;
use std::sync::Arc;
pub type Transaction = Arc<Mutex<kvs::Transaction>>;
pub(crate) type Transaction = Arc<Mutex<kvs::Transaction>>;

View file

@ -4,7 +4,7 @@ use crate::err::Error;
use crate::sql::value::Value;
use std::collections::BTreeMap;
pub type Variables = Option<BTreeMap<String, Value>>;
pub(crate) type Variables = Option<BTreeMap<String, Value>>;
pub(crate) trait Attach {
fn attach(self, ctx: Context) -> Result<Context, Error>;

View file

@ -11,6 +11,7 @@ use crate::sql::value::Value;
use channel::Sender;
impl<'a> Document<'a> {
#[allow(dead_code)]
pub(crate) async fn compute(
ctx: &Context<'_>,
opt: &Options,

View file

@ -12,7 +12,7 @@ use crate::sql::value::Value;
use std::borrow::Cow;
use std::sync::Arc;
pub struct Document<'a> {
pub(crate) struct Document<'a> {
pub(super) id: Option<Thing>,
pub(super) extras: Workable,
pub(super) current: Cow<'a, Value>,

View file

@ -1,4 +1,4 @@
pub use self::document::*;
pub(crate) use self::document::*;
#[cfg(feature = "parallel")]
mod compute;

View file

@ -5,8 +5,9 @@ use storekey::decode::Error as DecodeError;
use storekey::encode::Error as EncodeError;
use thiserror::Error;
/// An error originating from the SurrealDB client library.
/// An error originating from an embedded SurrealDB database.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
/// This error is used for ignoring a document when processing a query
#[doc(hidden)]

View file

@ -2,5 +2,5 @@
#[quickjs(bare)]
#[allow(non_upper_case_globals)]
pub mod package {
pub const version: &str = crate::VERSION;
pub const version: &str = crate::env::VERSION;
}

View file

@ -41,8 +41,8 @@ impl Datastore {
/// # Examples
///
/// ```rust,no_run
/// # use surrealdb::Datastore;
/// # use surrealdb::Error;
/// # use surrealdb::kvs::Datastore;
/// # use surrealdb::err::Error;
/// # #[tokio::main]
/// # async fn main() -> Result<(), Error> {
/// let ds = Datastore::new("memory").await?;
@ -53,8 +53,8 @@ impl Datastore {
/// Or to create a file-backed store:
///
/// ```rust,no_run
/// # use surrealdb::Datastore;
/// # use surrealdb::Error;
/// # use surrealdb::kvs::Datastore;
/// # use surrealdb::err::Error;
/// # #[tokio::main]
/// # async fn main() -> Result<(), Error> {
/// let ds = Datastore::new("file://temp.db").await?;
@ -65,8 +65,8 @@ impl Datastore {
/// Or to connect to a tikv-backed distributed store:
///
/// ```rust,no_run
/// # use surrealdb::Datastore;
/// # use surrealdb::Error;
/// # use surrealdb::kvs::Datastore;
/// # use surrealdb::err::Error;
/// # #[tokio::main]
/// # async fn main() -> Result<(), Error> {
/// let ds = Datastore::new("tikv://127.0.0.1:2379").await?;
@ -155,8 +155,8 @@ impl Datastore {
/// Create a new transaction on this datastore
///
/// ```rust,no_run
/// use surrealdb::Datastore;
/// use surrealdb::Error;
/// use surrealdb::kvs::Datastore;
/// use surrealdb::err::Error;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
@ -217,9 +217,9 @@ impl Datastore {
/// Parse and execute an SQL query
///
/// ```rust,no_run
/// use surrealdb::Datastore;
/// use surrealdb::Error;
/// use surrealdb::Session;
/// use surrealdb::kvs::Datastore;
/// use surrealdb::err::Error;
/// use surrealdb::dbs::Session;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
@ -265,9 +265,9 @@ impl Datastore {
/// Execute a pre-parsed SQL query
///
/// ```rust,no_run
/// use surrealdb::Datastore;
/// use surrealdb::Error;
/// use surrealdb::Session;
/// use surrealdb::kvs::Datastore;
/// use surrealdb::err::Error;
/// use surrealdb::dbs::Session;
/// use surrealdb::sql::parse;
///
/// #[tokio::main]
@ -312,9 +312,9 @@ impl Datastore {
/// Ensure a SQL [`Value`] is fully computed
///
/// ```rust,no_run
/// use surrealdb::Datastore;
/// use surrealdb::Error;
/// use surrealdb::Session;
/// use surrealdb::kvs::Datastore;
/// use surrealdb::err::Error;
/// use surrealdb::dbs::Session;
/// use surrealdb::sql::Future;
/// use surrealdb::sql::Value;
///

View file

@ -12,4 +12,4 @@ pub use self::ds::*;
pub use self::kv::*;
pub use self::tx::*;
pub const LOG: &str = "surrealdb::kvs";
pub(crate) const LOG: &str = "surrealdb::kvs";

View file

@ -308,8 +308,8 @@ mod tests {
let mut transaction = get_transaction().await;
transaction.put("uh", "oh").await.unwrap();
async fn get_transaction() -> crate::Transaction {
let datastore = crate::Datastore::new("rocksdb:/tmp/rocks.db").await.unwrap();
async fn get_transaction() -> crate::kvs::Transaction {
let datastore = crate::kvs::Datastore::new("rocksdb:/tmp/rocks.db").await.unwrap();
datastore.transaction(true, false).await.unwrap()
}
}

View file

@ -1,14 +1,104 @@
//! This library provides the low-level database library implementation, and query language
//! definition, for [SurrealDB](https://surrealdb.com), the ultimate cloud database for
//! This library provides a low-level database library implementation, a remote client
//! and a query language definition, for [SurrealDB](https://surrealdb.com), the ultimate cloud database for
//! tomorrow's applications. SurrealDB is a scalable, distributed, collaborative, document-graph
//! database for the realtime web.
//!
//! This library can be used to start an embedded in-memory datastore, an embedded datastore
//! persisted to disk, a browser-based embedded datastore backed by IndexedDB, or for connecting
//! to a distributed [TiKV](https://tikv.org) key-value store.
//!
//! It also enables simple and advanced querying of a remote SurrealDB server from
//! server-side or client-side code. All connections to SurrealDB are made over WebSockets by default,
//! and automatically reconnect when the connection is terminated.
//!
//! # Examples
//!
//! ```no_run
//! use serde::{Serialize, Deserialize};
//! use serde_json::json;
//! use std::borrow::Cow;
//! use surrealdb::{Result, Surreal};
//! use surrealdb::sql;
//! use surrealdb::opt::auth::Root;
//! use surrealdb::engines::remote::ws::Ws;
//!
//! #[derive(Serialize, Deserialize)]
//! struct Name {
//! first: Cow<'static, str>,
//! last: Cow<'static, str>,
//! }
//!
//! #[derive(Serialize, Deserialize)]
//! struct Person {
//! title: Cow<'static, str>,
//! name: Name,
//! marketing: bool,
//! }
//!
//! #[tokio::main]
//! async fn main() -> Result<()> {
//! let db = Surreal::new::<Ws>("localhost:8000").await?;
//!
//! // Signin as a namespace, database, or root user
//! db.signin(Root {
//! username: "root",
//! password: "root",
//! }).await?;
//!
//! // Select a specific namespace / database
//! db.use_ns("namespace").use_db("database").await?;
//!
//! // Create a new person with a random ID
//! let created: Person = db.create("person")
//! .content(Person {
//! title: "Founder & CEO".into(),
//! name: Name {
//! first: "Tobie".into(),
//! last: "Morgan Hitchcock".into(),
//! },
//! marketing: true,
//! })
//! .await?;
//!
//! // Create a new person with a specific ID
//! let created: Person = db.create(("person", "jaime"))
//! .content(Person {
//! title: "Founder & COO".into(),
//! name: Name {
//! first: "Jaime".into(),
//! last: "Morgan Hitchcock".into(),
//! },
//! marketing: false,
//! })
//! .await?;
//!
//! // Update a person record with a specific ID
//! let updated: Person = db.update(("person", "jaime"))
//! .merge(json!({"marketing": true}))
//! .await?;
//!
//! // Select all people records
//! let people: Vec<Person> = db.select("person").await?;
//!
//! // Perform a custom advanced query
//! let sql = sql! {
//! SELECT marketing, count()
//! FROM type::table($table)
//! GROUP BY marketing
//! };
//!
//! let groups = db.query(sql)
//! .bind(("table", "person"))
//! .await?;
//!
//! Ok(())
//! }
//! ```
#![doc(html_favicon_url = "https://surrealdb.s3.amazonaws.com/favicon.png")]
#![doc(html_logo_url = "https://surrealdb.s3.amazonaws.com/icon.png")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(test, deny(warnings))]
#[macro_use]
extern crate log;
@ -16,39 +106,63 @@ extern crate log;
#[macro_use]
mod mac;
mod api;
mod cnf;
mod ctx;
mod dbs;
mod doc;
mod err;
mod exe;
mod fnc;
mod key;
mod kvs;
// ENV
pub mod env;
// SQL
pub mod sql;
// Exports
pub use dbs::Auth;
pub use dbs::Response;
pub use dbs::Session;
pub use err::Error;
pub use kvs::Datastore;
pub use kvs::Key;
pub use kvs::Transaction;
pub use kvs::Val;
#[doc(hidden)]
pub mod dbs;
#[doc(hidden)]
pub mod env;
#[doc(hidden)]
pub mod err;
#[doc(hidden)]
pub mod kvs;
// Re-exports
#[doc(inline)]
pub use api::engines;
#[doc(inline)]
pub use api::method;
#[doc(inline)]
pub use api::opt;
#[doc(inline)]
pub use api::Connect;
#[doc(inline)]
pub use api::Connection;
#[doc(inline)]
pub use api::Response;
#[doc(inline)]
pub use api::Result;
#[doc(inline)]
pub use api::Surreal;
#[doc(hidden)]
/// Channels for receiving a SurrealQL database export
pub mod channel {
pub use channel::bounded as new;
pub use channel::Receiver;
pub use channel::Sender;
}
// Version
#[doc(inline)]
pub use env::VERSION;
/// Different error types for embedded and remote databases
pub mod error {
pub use crate::api::err::Error as Api;
pub use crate::err::Error as Db;
}
/// An error originating from the SurrealDB client library
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// An error with an embedded storage engine
#[error("Database error: {0}")]
Db(#[from] crate::error::Db),
/// An error with a remote database instance
#[error("API error: {0}")]
Api(#[from] crate::error::Api),
}

View file

@ -1,9 +1,11 @@
/// Converts some text into a new line byte string
macro_rules! bytes {
($expression:expr) => {
format!("{}\n", $expression).into_bytes()
};
}
/// Creates a new b-tree map of key-value pairs
macro_rules! map {
($($k:expr => $v:expr),* $(,)?) => {{
::std::collections::BTreeMap::from([
@ -12,8 +14,33 @@ macro_rules! map {
}};
}
/// Matches on a specific config environment
macro_rules! get_cfg {
($i:ident : $($s:expr),+) => (
let $i = || { $( if cfg!($i=$s) { return $s; } );+ "unknown"};
)
}
/// Parses a set of SurrealQL statements
///
/// # Examples
///
/// ```no_run
/// # use surrealdb::sql;
/// # fn main() -> surrealdb::Result<()> {
/// let query = sql! {
/// LET $name = "Tobie";
/// SELECT * FROM user WHERE name = $name;
/// };
/// # Ok(())
/// # }
/// ```
#[macro_export]
macro_rules! sql {
($($query:tt)*) => {
match $crate::sql::parse(stringify!($($query)*)) {
Ok(v) => v,
Err(e) => { return Err(e.into()); },
}
};
}

View file

@ -48,6 +48,12 @@ impl From<Vec<&str>> for Array {
}
}
impl From<Vec<String>> for Array {
fn from(v: Vec<String>) -> Self {
Self(v.into_iter().map(Value::from).collect())
}
}
impl From<Vec<Number>> for Array {
fn from(v: Vec<Number>) -> Self {
Self(v.into_iter().map(Value::from).collect())

View file

@ -81,6 +81,24 @@ impl From<&str> for Id {
}
}
impl From<&String> for Id {
fn from(v: &String) -> Self {
Self::String(v.to_owned())
}
}
impl From<Vec<&str>> for Id {
fn from(v: Vec<&str>) -> Self {
Id::Array(v.into())
}
}
impl From<Vec<String>> for Id {
fn from(v: Vec<String>) -> Self {
Id::Array(v.into())
}
}
impl From<Vec<Value>> for Id {
fn from(v: Vec<Value>) -> Self {
Id::Array(v.into())

View file

@ -1,3 +1,5 @@
//! The full type definitions for the SurrealQL query language
pub(crate) mod algorithm;
pub(crate) mod array;
pub(crate) mod base;

169
lib/tests/api.rs Normal file
View file

@ -0,0 +1,169 @@
#![allow(unused_imports, dead_code)]
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use std::borrow::Cow;
use std::ops::Bound;
use surrealdb::opt::auth::Database;
use surrealdb::opt::auth::Jwt;
use surrealdb::opt::auth::Namespace;
use surrealdb::opt::auth::Root;
use surrealdb::opt::auth::Scope;
use surrealdb::opt::PatchOp;
use surrealdb::sql::statements::BeginStatement;
use surrealdb::sql::statements::CommitStatement;
use surrealdb::Surreal;
use ulid::Ulid;
const NS: &str = "test-ns";
const ROOT_USER: &str = "root";
const ROOT_PASS: &str = "root";
#[derive(Debug, Serialize)]
struct Record<'a> {
name: &'a str,
}
#[derive(Debug, Deserialize)]
struct RecordId {
id: String,
}
#[derive(Debug, Deserialize)]
struct RecordName {
name: String,
}
#[derive(Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
struct RecordBuf {
id: String,
name: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct AuthParams<'a> {
email: &'a str,
pass: &'a str,
}
#[cfg(feature = "protocol-ws")]
mod ws {
use super::*;
use surrealdb::engines::remote::ws::Client;
use surrealdb::engines::remote::ws::Ws;
async fn new_db() -> Surreal<Client> {
let db = Surreal::new::<Ws>("127.0.0.1:8000").await.unwrap();
db.signin(Root {
username: ROOT_USER,
password: ROOT_PASS,
})
.await
.unwrap();
db
}
include!("api/mod.rs");
include!("api/auth.rs");
}
#[cfg(feature = "protocol-http")]
mod http {
use super::*;
use surrealdb::engines::remote::http::Client;
use surrealdb::engines::remote::http::Http;
async fn new_db() -> Surreal<Client> {
let db = Surreal::new::<Http>("127.0.0.1:8000").await.unwrap();
db.signin(Root {
username: ROOT_USER,
password: ROOT_PASS,
})
.await
.unwrap();
db
}
include!("api/mod.rs");
include!("api/auth.rs");
include!("api/backup.rs");
}
#[cfg(feature = "kv-mem")]
mod mem {
use super::*;
use surrealdb::engines::local::Db;
use surrealdb::engines::local::Mem;
async fn new_db() -> Surreal<Db> {
Surreal::new::<Mem>(()).await.unwrap()
}
include!("api/mod.rs");
include!("api/backup.rs");
}
#[cfg(feature = "kv-rocksdb")]
mod file {
use super::*;
use surrealdb::engines::local::Db;
use surrealdb::engines::local::File;
async fn new_db() -> Surreal<Db> {
let path = format!("/tmp/{}.db", Ulid::new());
Surreal::new::<File>(path.as_str()).await.unwrap()
}
include!("api/mod.rs");
include!("api/backup.rs");
}
#[cfg(feature = "kv-tikv")]
mod tikv {
use super::*;
use surrealdb::engines::local::Db;
use surrealdb::engines::local::TiKv;
async fn new_db() -> Surreal<Db> {
Surreal::new::<TiKv>("127.0.0.1:2379").await.unwrap()
}
include!("api/mod.rs");
include!("api/backup.rs");
}
#[cfg(feature = "kv-fdb")]
mod fdb {
use super::*;
use surrealdb::engines::local::Db;
use surrealdb::engines::local::FDb;
async fn new_db() -> Surreal<Db> {
Surreal::new::<FDb>("/tmp/fdb.cluster").await.unwrap()
}
include!("api/mod.rs");
include!("api/backup.rs");
}
#[cfg(feature = "protocol-http")]
mod any {
use super::*;
use surrealdb::engines::any::Any;
async fn new_db() -> Surreal<Any> {
let db = surrealdb::engines::any::connect("http://127.0.0.1:8000").await.unwrap();
db.signin(Root {
username: ROOT_USER,
password: ROOT_PASS,
})
.await
.unwrap();
db
}
include!("api/mod.rs");
include!("api/auth.rs");
include!("api/backup.rs");
}

138
lib/tests/api/auth.rs Normal file
View file

@ -0,0 +1,138 @@
// Auth tests
// Supported by both HTTP and WS protocols
#[tokio::test]
async fn invalidate() {
let db = new_db().await;
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
db.invalidate().await.unwrap();
let error = db.create::<Option<RecordId>>(("user", "john")).await.unwrap_err();
assert!(error.to_string().contains("You don't have permission to perform this query type"));
}
#[tokio::test]
async fn signup_scope() {
let db = new_db().await;
let database = Ulid::new().to_string();
db.use_ns(NS).use_db(&database).await.unwrap();
let scope = Ulid::new().to_string();
let sql = format!(
"
DEFINE SCOPE {scope} SESSION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
"
);
let response = db.query(sql).await.unwrap();
response.check().unwrap();
db.signup(Scope {
namespace: NS,
database: &database,
scope: &scope,
params: AuthParams {
email: "john.doe@example.com",
pass: "password123",
},
})
.await
.unwrap();
}
#[tokio::test]
async fn signin_ns() {
let db = new_db().await;
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
let user = Ulid::new().to_string();
let pass = "password123";
let sql = format!("DEFINE LOGIN {user} ON NAMESPACE PASSWORD '{pass}'");
let response = db.query(sql).await.unwrap();
response.check().unwrap();
db.signin(Namespace {
namespace: NS,
username: &user,
password: pass,
})
.await
.unwrap();
}
#[tokio::test]
async fn signin_db() {
let db = new_db().await;
let database = Ulid::new().to_string();
db.use_ns(NS).use_db(&database).await.unwrap();
let user = Ulid::new().to_string();
let pass = "password123";
let sql = format!("DEFINE LOGIN {user} ON DATABASE PASSWORD '{pass}'");
let response = db.query(sql).await.unwrap();
response.check().unwrap();
db.signin(Database {
namespace: NS,
database: &database,
username: &user,
password: pass,
})
.await
.unwrap();
}
#[tokio::test]
async fn signin_scope() {
let db = new_db().await;
let database = Ulid::new().to_string();
db.use_ns(NS).use_db(&database).await.unwrap();
let scope = Ulid::new().to_string();
let email = format!("{scope}@example.com");
let pass = "password123";
let sql = format!(
"
DEFINE SCOPE {scope} SESSION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
"
);
let response = db.query(sql).await.unwrap();
response.check().unwrap();
db.signup(Scope {
namespace: NS,
database: &database,
scope: &scope,
params: AuthParams {
pass,
email: &email,
},
})
.await
.unwrap();
db.signin(Scope {
namespace: NS,
database: &database,
scope: &scope,
params: AuthParams {
pass,
email: &email,
},
})
.await
.unwrap();
}
#[tokio::test]
async fn authenticate() {
let db = new_db().await;
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
let user = Ulid::new().to_string();
let pass = "password123";
let sql = format!("DEFINE LOGIN {user} ON NAMESPACE PASSWORD '{pass}'");
let response = db.query(sql).await.unwrap();
response.check().unwrap();
let token = db
.signin(Namespace {
namespace: NS,
username: &user,
password: pass,
})
.await
.unwrap();
db.authenticate(token).await.unwrap();
}

Some files were not shown because too many files have changed in this diff Show more