This content originally appeared on DEV Community and was authored by Romans Malinovskis
SurrealDB is a new multi-modal database, which is in active development. And while I am really impressed by the features – users report performance issues.
I also want to use SurrealDB in my project, but if we do run into performance issues – we better have a way to switch to a different database. A custom query language SurrealQL makes it pretty difficult to perform such a change. I’ve observed many users trying SurrealDB a year ago and having exactly the same issue.
I continue to refactor my project – Vantage, and today I have a very basic support for Query Building, but now it’s done in a vendor-independent way.
Query builders
Query builders that I’ve mentioned in my previous post typically support only a single query language. In Vantage my expression builder supports various vendors and query language dialects and therefore should be able to easily support transition between PostgreSQL and SurrealDB.
A new implementation of Expression engine (0.3) allows this behaviour and makes it much more intuitive to implement a query builder:
https://github.com/romaninsh/vantage/pull/53
As example, here is implementation of “identifiers”. In SQL if your column or table is using reserved keyword, you can use backtick
to escape it. While SurrealDB supports backticks, it also supports ⟨mytable⟩
. Here is a reference implementation for identifier escaping:
#[derive(Debug, Clone)]
pub struct Identifier {
identifier: String,
}
impl Identifier {
pub fn new(identifier: impl Into<String>) -> Self {
Self {
identifier: identifier.into(),
}
}
fn needs_escaping(&self) -> bool {
let reserved_keywords = [
"DEFINE", "CREATE", "SELECT", "UPDATE",
"DELETE", "FROM", "WHERE", "SET", "ONLY",
"TABLE",
];
let upper_identifier = self.identifier.to_uppercase();
// Check if it contains spaces or is a reserved keyword
self.identifier.contains(' ')
|| reserved_keywords.contains(&upper_identifier.as_str())
}
}
impl Into<OwnedExpression> for Identifier {
fn into(self) -> OwnedExpression {
if self.needs_escaping() {
expr!(format!("⟨{}⟩", self.identifier))
} else {
expr!(self.identifier)
}
}
}
This can then be intuitively used like this:
expr!(
"SELECT VALUE {} AS {}",
Identifier::new(base_expr),
Identifier::new(alias)
),
Now the render_field() construct can easily rely on identifier as needed:
fn render_fields(&self) -> OwnedExpression {
if self.fields.is_empty() {
expr!("*")
} else {
let field_expressions: Vec<OwnedExpression> = self
.fields
.iter()
.map(|(alias: &Option<String>, field: &OwnedExpression)| {
if let Some(alias) = alias {
expr!("{} AS {}", field.clone(), Identifier::new(alias.clone()))
} else {
field.clone()
}
})
.collect();
OwnedExpression::from_vec(field_expressions, ", ")
}
}
Code for each query-builder is clean and can focus on language features of specific query dialect, rather than string formatting.
Next, I’m going to revisit the concept of LazyExpressions some more and possibility to use cross-vendor queries in expressions seamlessly.
This content originally appeared on DEV Community and was authored by Romans Malinovskis