use nodedb_sql::ddl_ast::statement::{CollectionStmt, NodedbStatement};
use nodedb_types::result::QueryResult;
use crate::error::LiteError;
use crate::query::document_ops::indexes;
use crate::query::engine::LiteQueryEngine;
use crate::storage::engine::StorageEngine;
#[derive(Clone, Copy)]
enum DefaultEngine {
Document,
Strict,
}
fn requested_engine(engine: Option<&str>, options: &[(String, String)]) -> Option<String> {
engine
.map(|e| e.to_ascii_lowercase())
.or_else(|| {
options
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case("storage"))
.map(|(_, value)| value.to_ascii_lowercase())
})
.map(|value| value.trim_matches(['\'', '"']).to_string())
}
fn wants_bitemporal(options: &[(String, String)], flags: &[String]) -> bool {
if flags.iter().any(|f| f.eq_ignore_ascii_case("BITEMPORAL")) {
return true;
}
options.iter().any(|(key, value)| {
key.eq_ignore_ascii_case("bitemporal")
&& value.trim_matches(['\'', '"']).eq_ignore_ascii_case("true")
})
}
impl<S: StorageEngine> LiteQueryEngine<S> {
pub(in crate::query) async fn try_handle_ddl(
&self,
sql: &str,
) -> Option<Result<QueryResult, LiteError>> {
if let Some(statement) = super::search_index::parse_search_index_ddl(sql) {
return Some(match statement {
Ok(statement) => self.handle_search_index(statement).await,
Err(error) => Err(error),
});
}
if let Some(statement) = super::sorted_index::parse_sorted_index_sql(sql) {
return Some(match statement {
Ok(statement) => self.handle_sorted_index(statement).await,
Err(error) => Err(error),
});
}
let upper = sql.trim().to_uppercase();
if upper.starts_with("CREATE MATERIALIZED VIEW ") {
return Some(self.handle_create_materialized_view(sql).await);
}
if upper.starts_with("DROP MATERIALIZED VIEW ") {
return Some(self.handle_drop_materialized_view(sql).await);
}
if upper.starts_with("CREATE CONTINUOUS AGGREGATE ") {
return Some(self.handle_create_continuous_aggregate(sql).await);
}
if upper.starts_with("DROP CONTINUOUS AGGREGATE ") {
return Some(self.handle_drop_continuous_aggregate(sql).await);
}
if upper.starts_with("SHOW CONTINUOUS AGGREGATES") {
return Some(self.handle_show_continuous_aggregates(sql).await);
}
if upper.starts_with("CREATE TIMESERIES ") {
return Some(self.handle_create_timeseries(sql).await);
}
if upper.starts_with("CONVERT COLLECTION ") || upper.starts_with("CONVERT ") {
if upper.contains(" TO STRICT") {
return Some(self.handle_convert_to_strict(sql).await);
}
if upper.contains(" TO COLUMNAR") {
return Some(self.handle_convert_to_columnar(sql).await);
}
if upper.contains(" TO DOCUMENT") || upper.contains(" TO DOC") {
return Some(self.handle_convert_to_document(sql).await);
}
}
if upper.starts_with("ALTER TABLE ")
&& (upper.contains("ADD COLUMN") || upper.contains("ADD "))
{
return Some(self.handle_alter_add_column(sql).await);
}
let parsed = nodedb_sql::ddl_ast::parse(sql)?;
let statement = match parsed {
Ok(statement) => statement,
Err(e) => return Some(Err(LiteError::Query(e.to_string()))),
};
let NodedbStatement::Collection(collection) = statement else {
return None;
};
match collection {
CollectionStmt::CreateCollection {
name,
engine,
options,
flags,
..
} => Some(
self.dispatch_create_collection(
sql,
&name,
engine.as_deref(),
&options,
&flags,
DefaultEngine::Document,
)
.await,
),
CollectionStmt::CreateTable {
name,
engine,
options,
flags,
..
} => Some(
self.dispatch_create_collection(
sql,
&name,
engine.as_deref(),
&options,
&flags,
DefaultEngine::Strict,
)
.await,
),
CollectionStmt::DropCollection { name, .. } => {
let name = name.to_lowercase();
if self.strict.schema(&name).is_some() {
return Some(self.handle_drop_strict(&name).await);
}
if self.columnar.schema(&name).is_some() {
return Some(self.handle_drop_columnar(&name).await);
}
Some(self.handle_drop_document(&name).await)
}
CollectionStmt::CreateIndex {
unique,
index_name,
collection,
field,
case_insensitive,
where_condition,
if_not_exists,
} => Some(
indexes::create_index(
self,
indexes::CreateIndexRequest {
name: index_name.as_deref(),
collection: &collection,
field: &field,
unique,
case_insensitive,
predicate: where_condition.as_deref(),
if_not_exists,
},
)
.await,
),
CollectionStmt::DropIndex {
name, if_exists, ..
} => Some(indexes::drop_index(self, &name, if_exists).await),
CollectionStmt::Reindex {
collection,
index_name,
..
} => Some(indexes::reindex(self, &collection, index_name.as_deref()).await),
CollectionStmt::DescribeCollection { name } => {
let name = name.to_lowercase();
let schema = self.strict.schema(&name)?;
Some(Ok(super::describe_strict_collection(&name, &schema)))
}
_ => None,
}
}
async fn dispatch_create_collection(
&self,
sql: &str,
name: &str,
engine: Option<&str>,
options: &[(String, String)],
flags: &[String],
default_engine: DefaultEngine,
) -> Result<QueryResult, LiteError> {
let name = name.to_lowercase();
if let Some(engine) = requested_engine(engine, options) {
return match engine.as_str() {
"strict" | "document_strict" => self.handle_create_strict(sql).await,
"columnar" => self.handle_create_columnar(sql).await,
"kv" => self.handle_create_kv(sql).await,
"timeseries" => self.handle_create_timeseries(sql).await,
"document" | "document_schemaless" => {
if wants_bitemporal(options, flags) {
self.handle_create_bitemporal_document(&name).await
} else {
self.handle_create_document(&name).await
}
}
other => Err(LiteError::Query(format!(
"unsupported engine '{other}' for collection '{name}'"
))),
};
}
match default_engine {
DefaultEngine::Strict => self.handle_create_strict(sql).await,
DefaultEngine::Document => {
if wants_bitemporal(options, flags) {
self.handle_create_bitemporal_document(&name).await
} else {
self.handle_create_document(&name).await
}
}
}
}
}